├── .cargo └── config.toml ├── .github ├── codecov.yml ├── renovate.json └── workflows │ ├── ci.yml │ ├── coverage.yml │ ├── docs.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── Justfile ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── release.toml ├── scripts ├── cargo-release-publish.sh └── fix-readmes.awk ├── src ├── data_source.rs ├── lib.rs ├── macros.rs └── runner.rs └── tests ├── compile-fail ├── empty-arg-list.rs ├── empty-arg-list.stderr ├── extra-args.rs ├── extra-args.stderr ├── incorrect-arg-in-pattern.rs ├── incorrect-arg-in-pattern.stderr ├── missing-root-comma.rs ├── missing-root-comma.stderr ├── missing-root-no-args.rs ├── missing-root-no-args.stderr ├── missing-root-no-comma.rs ├── missing-root-no-comma.stderr ├── missing-test-comma.rs ├── missing-test-comma.stderr ├── missing-test-no-comma.rs ├── missing-test-no-comma.stderr ├── old-format.rs ├── old-format.stderr ├── pattern-not-ident.rs ├── pattern-not-ident.stderr ├── root-not-ident.rs ├── root-not-ident.stderr ├── root-out-of-order.rs ├── root-out-of-order.stderr ├── test-not-ident.rs ├── test-not-ident.stderr ├── test-out-of-order.rs └── test-out-of-order.stderr ├── example.rs ├── files ├── b.txt ├── c.skip.txt ├── dir │ └── a.txt └── other.json └── integration.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | xfmt = "fmt -- --config imports_granularity=Crate --config group_imports=One" 3 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | informational: true 6 | patch: 7 | default: 8 | target: 0% 9 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>nextest-rs/renovate"] 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | branches: 7 | - main 8 | 9 | name: CI 10 | env: 11 | RUSTFLAGS: -D warnings 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | lint: 16 | name: Lint 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 20 | - uses: dtolnay/rust-toolchain@stable 21 | with: 22 | components: rustfmt, clippy 23 | - uses: Swatinem/rust-cache@720f7e45ccee46c12a7b1d7bed2ab733be9be5a1 # v2 24 | - name: Lint (clippy) 25 | run: cargo clippy --all-features --all-targets 26 | - name: Lint (rustfmt) 27 | run: cargo xfmt --check 28 | - name: Install cargo-sync-rdme and just 29 | uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2 30 | with: 31 | tool: cargo-sync-rdme,just 32 | - name: Install nightly toolchain for cargo-sync-rdme 33 | uses: dtolnay/rust-toolchain@nightly 34 | - name: Generate readmes 35 | run: just generate-readmes 36 | - name: Check for differences 37 | run: git diff --exit-code 38 | 39 | build-rustdoc: 40 | name: Build documentation 41 | runs-on: ${{ matrix.os }} 42 | strategy: 43 | matrix: 44 | os: [ubuntu-latest] 45 | fail-fast: false 46 | steps: 47 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 48 | - uses: dtolnay/rust-toolchain@stable 49 | with: 50 | components: rustfmt, clippy 51 | - uses: Swatinem/rust-cache@720f7e45ccee46c12a7b1d7bed2ab733be9be5a1 # v2 52 | - name: Build rustdoc 53 | run: cargo doc --all-features 54 | 55 | build: 56 | name: Build and test 57 | runs-on: ${{ matrix.os }} 58 | strategy: 59 | matrix: 60 | os: 61 | - ubuntu-latest 62 | - windows-latest 63 | rust-version: ["1.72", stable] 64 | fail-fast: false 65 | steps: 66 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 67 | - uses: dtolnay/rust-toolchain@master 68 | with: 69 | toolchain: ${{ matrix.rust-version }} 70 | - uses: Swatinem/rust-cache@720f7e45ccee46c12a7b1d7bed2ab733be9be5a1 # v2 71 | with: 72 | key: ${{ matrix.rust-version }} 73 | - name: Install tools 74 | uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2 75 | with: 76 | tool: cargo-hack,just,nextest 77 | - name: Build 78 | run: just powerset build 79 | - name: Build with all targets 80 | run: just powerset build --all-targets 81 | - name: Run tests 82 | run: just powerset nextest run 83 | - name: Run tests with cargo test 84 | run: just powerset test 85 | 86 | # Remove Cargo.lock to ensure that building with the latest versions works on stable. 87 | - name: Remove Cargo.lock and rebuild on stable 88 | if: matrix.rust-version == 'stable' 89 | run: rm Cargo.lock && cargo build 90 | - name: Build with all targets 91 | if: matrix.rust-version == 'stable' 92 | run: just powerset build --all-targets 93 | - name: Run tests on stable 94 | if: matrix.rust-version == 'stable' 95 | run: just powerset nextest run 96 | - name: Run tests with cargo test on stable 97 | if: matrix.rust-version == 'stable' 98 | run: just powerset test 99 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | branches: 7 | - main 8 | 9 | name: Test coverage 10 | 11 | jobs: 12 | coverage: 13 | name: Collect test coverage 14 | runs-on: ubuntu-latest 15 | # nightly rust might break from time to time 16 | continue-on-error: true 17 | env: 18 | RUSTFLAGS: -D warnings 19 | CARGO_TERM_COLOR: always 20 | steps: 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 22 | # Nightly Rust is used for cargo llvm-cov --doc below. 23 | - uses: dtolnay/rust-toolchain@nightly 24 | with: 25 | components: llvm-tools-preview 26 | - uses: Swatinem/rust-cache@720f7e45ccee46c12a7b1d7bed2ab733be9be5a1 # v2 27 | 28 | - name: Install tools 29 | uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2 30 | with: 31 | tool: cargo-llvm-cov,just,nextest 32 | 33 | - name: Collect coverage data 34 | run: | 35 | just coverage --lcov --output-path lcov.info 36 | - name: Upload coverage data to codecov 37 | uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5 38 | env: 39 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 40 | with: 41 | files: lcov.info 42 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | name: Docs 7 | 8 | jobs: 9 | docs: 10 | name: Build and deploy documentation 11 | concurrency: ci-${{ github.ref }} 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 15 | - uses: dtolnay/rust-toolchain@stable 16 | - uses: taiki-e/install-action@just 17 | - name: Build rustdoc 18 | run: just rustdoc 19 | - name: Organize 20 | run: | 21 | rm -rf target/gh-pages 22 | mkdir target/gh-pages 23 | mv target/doc/_redirects target/gh-pages 24 | mv target/doc target/gh-pages/rustdoc 25 | - name: Publish 26 | uses: cloudflare/pages-action@1 27 | with: 28 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} 29 | accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 30 | projectName: datatest-stable 31 | directory: target/gh-pages 32 | gitHubToken: ${{ secrets.GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # adapted from https://github.com/taiki-e/cargo-hack/blob/main/.github/workflows/release.yml 2 | 3 | name: Publish releases to GitHub 4 | on: 5 | push: 6 | tags: 7 | - "*" 8 | 9 | jobs: 10 | datatest-stable-release: 11 | if: github.repository_owner == 'nextest-rs' && startsWith(github.ref_name, 'datatest-stable-') 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 15 | with: 16 | persist-credentials: false 17 | - name: Install Rust 18 | uses: dtolnay/rust-toolchain@stable 19 | - name: Install cargo release 20 | uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2 21 | with: 22 | tool: cargo-release@0.25.0 23 | - uses: taiki-e/create-gh-release-action@26b80501670402f1999aff4b934e1574ef2d3705 # v1 24 | with: 25 | prefix: datatest-stable 26 | changelog: CHANGELOG.md 27 | title: $prefix $version 28 | branch: main 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | - run: ./scripts/cargo-release-publish.sh 32 | env: 33 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.3.2] - 2024-12-28 4 | 5 | ### Added 6 | 7 | - `pattern` is now optional in the `harness!` macro. If not specified, the default pattern is 8 | `r".*"` (match all files). 9 | 10 | ### Fixed 11 | 12 | - Restored the ability to use absolute paths as the `root` argument. 13 | 14 | ## [0.3.1] - 2024-12-25 15 | 16 | ### Fixed 17 | 18 | Fixed documentation for `include_dir` invocations. They must typically be called 19 | via `include_dir!("$CARGO_MANIFEST_DIR/path/to/data")`. 20 | 21 | ## [0.3.0] - 2024-12-24 22 | 23 | ### Added 24 | 25 | - Support for embedding data into the test binary via an optional `include-dir` 26 | feature. For more information and recommendations for when to use this, see [the 27 | readme](https://crates.io/crates/datatest-stable). 28 | 29 | ### Changed 30 | 31 | - The macro call format has changed to: 32 | 33 | ```rust 34 | datatest_stable::harness! { 35 | { test = fn_name, path = &include_dir!("path/to/data"), pattern = r"^.*$" }, 36 | // ... 37 | } 38 | ``` 39 | 40 | This is both a nicer format for expressing multiple tests, and a signal to 41 | indicate the other breaking changes in this release. 42 | 43 | - Regex patterns now match against the path relative to the *include directory*, rather 44 | than paths with respect to the crate root. This change was made for uniformity with the 45 | `include_dir` implementation. 46 | 47 | - On Windows, paths are now universally normalized to use forward slashes. This change 48 | was made to ensure that test names and paths are consistent across platforms. 49 | 50 | ## [0.2.10] - 2024-12-08 51 | 52 | - Internal dependency updates: update `libtest-mimic` to 0.8.1, and `fancy-regex` to 0.14.0. 53 | - Update MSRV to Rust 1.72. 54 | 55 | ## [0.2.9] - 2024-04-25 56 | 57 | ### Added 58 | 59 | Previously, the test functions supported were `fn(&Path) -> Result<()>` and `fn(&Utf8Path) -> Result<()>`. This release adds additional supported functions: 60 | 61 | - `fn(&P, String) -> datatest_stable::Result<()>` where `P` is `Path` or `Utf8Path`. If the 62 | extra `String` parameter is specified, the contents of the file will be loaded and passed in 63 | as a string (erroring out if that failed). 64 | - `fn(&P, Vec) -> datatest_stable::Result<()>` where `P` is `Path` or `Utf8Path`. If the 65 | extra `Vec` parameter is specified, the contents of the file will be 66 | loaded and passed in as a `Vec` (erroring out if that failed). 67 | 68 | ## [0.2.8] - 2024-04-24 69 | 70 | ### Fixed 71 | 72 | - Fixed quadratic performance issue with nextest, where datatest-stable would iterate over the 73 | entire list of files for each test. Thanks [@zaneduffield](https://github.com/zaneduffield) for 74 | your first contribution! 75 | 76 | ## [0.2.7] - 2024-04-21 77 | 78 | ### Changed 79 | 80 | - Switched to the `fancy-regex` crate, which allows for matching against regexes with 81 | lookahead/behind and backreferences. Thanks [@webbdays](https://github.com/webbdays) for your 82 | first contribution! 83 | 84 | - MSRV updated to Rust 1.66. 85 | 86 | ## [0.2.6] - 2024-04-09 87 | 88 | - Update to `libtest-mimic 0.7.2`, and use the upstream implementation of `ExitCode`. 89 | 90 | ## [0.2.5] - 2024-04-08 91 | 92 | - Exit main via `ExitCode` rather than `std::process::exit()`. This appears to fix coverage on 93 | Windows. 94 | 95 | ## [0.2.4] - 2024-04-08 96 | 97 | This is a periodic maintenance release. 98 | 99 | - Update internal dependency versions, including libtest-mimic to 0.7.0. 100 | - Update "docs (main)" link to the new location at [https://datatest-stable.nexte.st](https://datatest-stable.nexte.st). 101 | - Update MSRV to Rust 1.65. 102 | 103 | ## [0.2.3] - 2023-08-29 104 | 105 | Updated README. 106 | 107 | ## [0.2.2] - 2023-08-29 108 | 109 | ### Added 110 | 111 | - Restored compatibility with `fn(&Path) -> Result<()>`. The harness now can take either `fn(&Path) -> Result<()>` or `fn(&Utf8Path) -> Result<()>`. 112 | 113 | ## [0.2.1] - 2023-08-29 114 | 115 | ### Changed 116 | 117 | - The test signature is now `fn(&`[`Utf8Path`]`)` rather than `fn(&Path)`. If necessary, a `Utf8Path` can be converted to a `&Path` with [`.as_ref()`] or [`.as_std_path()`]. 118 | - Non-Unicode paths now consistently produce errors. Previously, the treatment of such paths was inconsistent -- they would either be skipped or produce errors. 119 | - Internal dependency update: libtest-mimic updated to version 0.6.1. 120 | - MSRV updated to Rust 1.60. 121 | 122 | [`Utf8Path`]: https://docs.rs/camino/latest/camino/struct.Utf8Path.html 123 | [`.as_ref()`]: https://docs.rs/camino/latest/camino/struct.Utf8Path.html#impl-AsRef%3COsStr%3E-for-Utf8Path 124 | [`.as_std_path()`]: https://docs.rs/camino/latest/camino/struct.Utf8Path.html#method.as_std_path 125 | 126 | ## [0.2.0] - 2023-08-29 127 | 128 | This version had a publishing issue. 129 | 130 | ## [0.1.3] - 2022-08-15 131 | 132 | ### Changed 133 | 134 | - Errors are now displayed with the `Debug` implementation, which prints out the full error chain 135 | with libraries like `anyhow` or `eyre`, rather than the `Display` implementation. Thanks 136 | [Alex Badics] for your first contribution! 137 | - MSRV updated to Rust 1.58. 138 | 139 | ### Internal improvements 140 | 141 | - datatest-stable now uses libtest-mimic 0.5.2. Thanks [Lukas Kalbertodt] (maintainer of 142 | libtest-mimic) for your first contribution! 143 | 144 | [Alex Badics]: https://github.com/badicsalex 145 | [Lukas]: https://github.com/LukasKalbertodt 146 | 147 | ## [0.1.2] - 2022-05-22 148 | 149 | ### Changed 150 | 151 | - New internal implementation, based on top of [libtest-mimic](https://github.com/LukasKalbertodt/libtest-mimic). 152 | - Repository updated to [nextest-rs/datatest-stable](https://github.com/nextest-rs/datatest-stable). 153 | - MSRV updated to Rust 1.56. 154 | 155 | There are no functional changes in this release. 156 | 157 | ## [0.1.1] - 2021-04-16 158 | 159 | ### Added 160 | 161 | - Initial release with basic support for data-driven tests. 162 | 163 | (Version 0.1.0 was yanked because of a metadata issue.) 164 | 165 | [0.3.2]: https://github.com/nextest-rs/datatest-stable/releases/tag/datatest-stable-0.3.2 166 | [0.3.1]: https://github.com/nextest-rs/datatest-stable/releases/tag/datatest-stable-0.3.1 167 | [0.3.0]: https://github.com/nextest-rs/datatest-stable/releases/tag/datatest-stable-0.3.0 168 | [0.2.10]: https://github.com/nextest-rs/datatest-stable/releases/tag/datatest-stable-0.2.10 169 | [0.2.9]: https://github.com/nextest-rs/datatest-stable/releases/tag/datatest-stable-0.2.9 170 | [0.2.8]: https://github.com/nextest-rs/datatest-stable/releases/tag/datatest-stable-0.2.8 171 | [0.2.7]: https://github.com/nextest-rs/datatest-stable/releases/tag/datatest-stable-0.2.7 172 | [0.2.6]: https://github.com/nextest-rs/datatest-stable/releases/tag/datatest-stable-0.2.6 173 | [0.2.5]: https://github.com/nextest-rs/datatest-stable/releases/tag/datatest-stable-0.2.5 174 | [0.2.4]: https://github.com/nextest-rs/datatest-stable/releases/tag/datatest-stable-0.2.4 175 | [0.2.3]: https://github.com/nextest-rs/datatest-stable/releases/tag/datatest-stable-0.2.3 176 | [0.2.2]: https://github.com/nextest-rs/datatest-stable/releases/tag/datatest-stable-0.2.2 177 | [0.2.1]: https://github.com/nextest-rs/datatest-stable/releases/tag/datatest-stable-0.2.1 178 | [0.1.3]: https://github.com/nextest-rs/datatest-stable/releases/tag/datatest-stable-0.1.3 179 | [0.1.2]: https://github.com/nextest-rs/datatest-stable/releases/tag/datatest-stable-0.1.2 180 | [0.1.1]: https://github.com/nextest-rs/datatest-stable/releases/tag/datatest-stable-0.1.1 181 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | datatest-stable is designed to work with both `cargo test` and `cargo nextest`. In either case, 4 | nextest is required for one of the tests. Install it via [the instructions here](https://get.nexte.st). 5 | 6 | ## Pull Requests 7 | 8 | If you have a new feature in mind, please discuss the feature in an issue to ensure that your 9 | contributions will be accepted. 10 | 11 | 1. Fork the repo and create your branch from `main`. 12 | 2. If you've added code that should be tested, add tests. 13 | 3. If you've changed APIs, update the documentation. 14 | 4. Ensure the test suite passes with `cargo nextest run --all-features`. 15 | 5. Run `cargo xfmt` to automatically format your changes (CI will let you know if you missed this). 16 | 17 | ## Logically Separate Commits 18 | 19 | As far as possible, please try and make commits 20 | [atomic](https://en.wikipedia.org/wiki/Atomic_commit#Atomic_commit_convention) and logically 21 | separate. We understand that GitHub's pull request model doesn't work well with "stacked diffs", so 22 | if your changes are complex, then a single PR with a series of commits is preferred. 23 | 24 | ## Bisectable History 25 | 26 | It is important that the project history is bisectable, so that when regressions are identified we 27 | can easily use `git bisect` to be able to pinpoint the exact commit which introduced the regression. 28 | We'll land your commits with: 29 | 30 | - "Rebase and merge" if your commits are atomic and each commit passes CI. 31 | - "Squash and merge" if they are not. 32 | 33 | ## Maintainers Editing Commits 34 | 35 | For efficiency reasons, maintainers may choose to edit your commits before landing them. The commits 36 | will still be credited to you, and the edits will be limited to reasonable changes that are in the 37 | spirit of the PR. (Think of the changes that the maintainers would have done anyway.) 38 | 39 | To make this easier, please check the box that allows maintainers to edit your branch: 40 | 41 | ![Screenshot of GitHub new pull request page, showing "Allow edits and access to secrets by maintainers" checked](https://github.com/nextest-rs/quick-junit/assets/180618/9f4074fa-4f52-4735-af19-144464f0fb8d) 42 | 43 | If maintainers need to make changes and that box is checked, then your PR can be marked as "merged" 44 | in the web UI. Otherwise, it will be marked as "closed". 45 | 46 | ## License 47 | 48 | By contributing to `datatest-stable`, you agree that your contributions will be dual-licensed under 49 | the terms of the [`LICENSE-MIT`](LICENSE-MIT) and [`LICENSE-APACHE`](LICENSE-APACHE) files in the 50 | root directory of this source tree. 51 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.3.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon 1.0.2", 24 | "colorchoice", 25 | "is-terminal", 26 | "utf8parse", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.6.18" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon 3.0.6", 39 | "colorchoice", 40 | "is_terminal_polyfill", 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle" 46 | version = "1.0.10" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 49 | 50 | [[package]] 51 | name = "anstyle-parse" 52 | version = "0.2.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" 55 | dependencies = [ 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-query" 61 | version = "1.0.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 64 | dependencies = [ 65 | "windows-sys 0.48.0", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-wincon" 70 | version = "1.0.2" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" 73 | dependencies = [ 74 | "anstyle", 75 | "windows-sys 0.48.0", 76 | ] 77 | 78 | [[package]] 79 | name = "anstyle-wincon" 80 | version = "3.0.6" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" 83 | dependencies = [ 84 | "anstyle", 85 | "windows-sys 0.59.0", 86 | ] 87 | 88 | [[package]] 89 | name = "bit-set" 90 | version = "0.8.0" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" 93 | dependencies = [ 94 | "bit-vec", 95 | ] 96 | 97 | [[package]] 98 | name = "bit-vec" 99 | version = "0.8.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" 102 | 103 | [[package]] 104 | name = "bitflags" 105 | version = "2.5.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 108 | 109 | [[package]] 110 | name = "camino" 111 | version = "1.1.9" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" 114 | 115 | [[package]] 116 | name = "camino-tempfile" 117 | version = "1.1.1" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "cb905055fa81e4d427f919b2cd0d76a998267de7d225ea767a1894743a5263c2" 120 | dependencies = [ 121 | "camino", 122 | "tempfile", 123 | ] 124 | 125 | [[package]] 126 | name = "cfg-if" 127 | version = "1.0.0" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 130 | 131 | [[package]] 132 | name = "clap" 133 | version = "4.3.24" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "fb690e81c7840c0d7aade59f242ea3b41b9bc27bcd5997890e7702ae4b32e487" 136 | dependencies = [ 137 | "clap_builder", 138 | "clap_derive", 139 | "once_cell", 140 | ] 141 | 142 | [[package]] 143 | name = "clap_builder" 144 | version = "4.3.24" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "5ed2e96bc16d8d740f6f48d663eddf4b8a0983e79210fd55479b7bcd0a69860e" 147 | dependencies = [ 148 | "anstream 0.3.2", 149 | "anstyle", 150 | "clap_lex", 151 | "strsim", 152 | ] 153 | 154 | [[package]] 155 | name = "clap_derive" 156 | version = "4.3.12" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" 159 | dependencies = [ 160 | "heck", 161 | "proc-macro2", 162 | "quote", 163 | "syn", 164 | ] 165 | 166 | [[package]] 167 | name = "clap_lex" 168 | version = "0.5.0" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" 171 | 172 | [[package]] 173 | name = "colorchoice" 174 | version = "1.0.0" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 177 | 178 | [[package]] 179 | name = "datatest-stable" 180 | version = "0.3.2" 181 | dependencies = [ 182 | "camino", 183 | "camino-tempfile", 184 | "fancy-regex", 185 | "fs_extra", 186 | "include_dir", 187 | "libtest-mimic", 188 | "trybuild", 189 | "walkdir", 190 | ] 191 | 192 | [[package]] 193 | name = "equivalent" 194 | version = "1.0.1" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 197 | 198 | [[package]] 199 | name = "errno" 200 | version = "0.3.8" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" 203 | dependencies = [ 204 | "libc", 205 | "windows-sys 0.52.0", 206 | ] 207 | 208 | [[package]] 209 | name = "escape8259" 210 | version = "0.5.2" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "ba4f4911e3666fcd7826997b4745c8224295a6f3072f1418c3067b97a67557ee" 213 | dependencies = [ 214 | "rustversion", 215 | ] 216 | 217 | [[package]] 218 | name = "fancy-regex" 219 | version = "0.14.0" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" 222 | dependencies = [ 223 | "bit-set", 224 | "regex-automata", 225 | "regex-syntax", 226 | ] 227 | 228 | [[package]] 229 | name = "fastrand" 230 | version = "2.0.2" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" 233 | 234 | [[package]] 235 | name = "fs_extra" 236 | version = "1.3.0" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" 239 | 240 | [[package]] 241 | name = "glob" 242 | version = "0.3.1" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 245 | 246 | [[package]] 247 | name = "hashbrown" 248 | version = "0.15.2" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 251 | 252 | [[package]] 253 | name = "heck" 254 | version = "0.4.1" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 257 | 258 | [[package]] 259 | name = "hermit-abi" 260 | version = "0.3.9" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 263 | 264 | [[package]] 265 | name = "include_dir" 266 | version = "0.7.4" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" 269 | dependencies = [ 270 | "include_dir_macros", 271 | ] 272 | 273 | [[package]] 274 | name = "include_dir_macros" 275 | version = "0.7.4" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" 278 | dependencies = [ 279 | "proc-macro2", 280 | "quote", 281 | ] 282 | 283 | [[package]] 284 | name = "indexmap" 285 | version = "2.7.0" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" 288 | dependencies = [ 289 | "equivalent", 290 | "hashbrown", 291 | ] 292 | 293 | [[package]] 294 | name = "is-terminal" 295 | version = "0.4.12" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" 298 | dependencies = [ 299 | "hermit-abi", 300 | "libc", 301 | "windows-sys 0.52.0", 302 | ] 303 | 304 | [[package]] 305 | name = "is_terminal_polyfill" 306 | version = "1.70.1" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 309 | 310 | [[package]] 311 | name = "itoa" 312 | version = "1.0.14" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 315 | 316 | [[package]] 317 | name = "libc" 318 | version = "0.2.153" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 321 | 322 | [[package]] 323 | name = "libtest-mimic" 324 | version = "0.8.1" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "5297962ef19edda4ce33aaa484386e0a5b3d7f2f4e037cbeee00503ef6b29d33" 327 | dependencies = [ 328 | "anstream 0.6.18", 329 | "anstyle", 330 | "clap", 331 | "escape8259", 332 | ] 333 | 334 | [[package]] 335 | name = "linux-raw-sys" 336 | version = "0.4.13" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" 339 | 340 | [[package]] 341 | name = "memchr" 342 | version = "2.7.2" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 345 | 346 | [[package]] 347 | name = "once_cell" 348 | version = "1.19.0" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 351 | 352 | [[package]] 353 | name = "proc-macro2" 354 | version = "1.0.92" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 357 | dependencies = [ 358 | "unicode-ident", 359 | ] 360 | 361 | [[package]] 362 | name = "quote" 363 | version = "1.0.35" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 366 | dependencies = [ 367 | "proc-macro2", 368 | ] 369 | 370 | [[package]] 371 | name = "regex-automata" 372 | version = "0.4.6" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" 375 | dependencies = [ 376 | "aho-corasick", 377 | "memchr", 378 | "regex-syntax", 379 | ] 380 | 381 | [[package]] 382 | name = "regex-syntax" 383 | version = "0.8.3" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" 386 | 387 | [[package]] 388 | name = "rustix" 389 | version = "0.38.34" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" 392 | dependencies = [ 393 | "bitflags", 394 | "errno", 395 | "libc", 396 | "linux-raw-sys", 397 | "windows-sys 0.52.0", 398 | ] 399 | 400 | [[package]] 401 | name = "rustversion" 402 | version = "1.0.15" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" 405 | 406 | [[package]] 407 | name = "ryu" 408 | version = "1.0.18" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 411 | 412 | [[package]] 413 | name = "same-file" 414 | version = "1.0.6" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 417 | dependencies = [ 418 | "winapi-util", 419 | ] 420 | 421 | [[package]] 422 | name = "serde" 423 | version = "1.0.216" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" 426 | dependencies = [ 427 | "serde_derive", 428 | ] 429 | 430 | [[package]] 431 | name = "serde_derive" 432 | version = "1.0.216" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" 435 | dependencies = [ 436 | "proc-macro2", 437 | "quote", 438 | "syn", 439 | ] 440 | 441 | [[package]] 442 | name = "serde_json" 443 | version = "1.0.134" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" 446 | dependencies = [ 447 | "itoa", 448 | "memchr", 449 | "ryu", 450 | "serde", 451 | ] 452 | 453 | [[package]] 454 | name = "serde_spanned" 455 | version = "0.6.8" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 458 | dependencies = [ 459 | "serde", 460 | ] 461 | 462 | [[package]] 463 | name = "strsim" 464 | version = "0.10.0" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 467 | 468 | [[package]] 469 | name = "syn" 470 | version = "2.0.91" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" 473 | dependencies = [ 474 | "proc-macro2", 475 | "quote", 476 | "unicode-ident", 477 | ] 478 | 479 | [[package]] 480 | name = "target-triple" 481 | version = "0.1.3" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "42a4d50cdb458045afc8131fd91b64904da29548bcb63c7236e0844936c13078" 484 | 485 | [[package]] 486 | name = "tempfile" 487 | version = "3.10.1" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" 490 | dependencies = [ 491 | "cfg-if", 492 | "fastrand", 493 | "rustix", 494 | "windows-sys 0.52.0", 495 | ] 496 | 497 | [[package]] 498 | name = "termcolor" 499 | version = "1.4.1" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 502 | dependencies = [ 503 | "winapi-util", 504 | ] 505 | 506 | [[package]] 507 | name = "toml" 508 | version = "0.8.19" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" 511 | dependencies = [ 512 | "serde", 513 | "serde_spanned", 514 | "toml_datetime", 515 | "toml_edit", 516 | ] 517 | 518 | [[package]] 519 | name = "toml_datetime" 520 | version = "0.6.8" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 523 | dependencies = [ 524 | "serde", 525 | ] 526 | 527 | [[package]] 528 | name = "toml_edit" 529 | version = "0.22.22" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" 532 | dependencies = [ 533 | "indexmap", 534 | "serde", 535 | "serde_spanned", 536 | "toml_datetime", 537 | "winnow", 538 | ] 539 | 540 | [[package]] 541 | name = "trybuild" 542 | version = "1.0.105" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "1c9bf9513a2f4aeef5fdac8677d7d349c79fdbcc03b9c86da6e9d254f1e43be2" 545 | dependencies = [ 546 | "glob", 547 | "serde", 548 | "serde_derive", 549 | "serde_json", 550 | "target-triple", 551 | "termcolor", 552 | "toml", 553 | ] 554 | 555 | [[package]] 556 | name = "unicode-ident" 557 | version = "1.0.12" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 560 | 561 | [[package]] 562 | name = "utf8parse" 563 | version = "0.2.1" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 566 | 567 | [[package]] 568 | name = "walkdir" 569 | version = "2.5.0" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 572 | dependencies = [ 573 | "same-file", 574 | "winapi-util", 575 | ] 576 | 577 | [[package]] 578 | name = "winapi" 579 | version = "0.3.9" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 582 | dependencies = [ 583 | "winapi-i686-pc-windows-gnu", 584 | "winapi-x86_64-pc-windows-gnu", 585 | ] 586 | 587 | [[package]] 588 | name = "winapi-i686-pc-windows-gnu" 589 | version = "0.4.0" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 592 | 593 | [[package]] 594 | name = "winapi-util" 595 | version = "0.1.6" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 598 | dependencies = [ 599 | "winapi", 600 | ] 601 | 602 | [[package]] 603 | name = "winapi-x86_64-pc-windows-gnu" 604 | version = "0.4.0" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 607 | 608 | [[package]] 609 | name = "windows-sys" 610 | version = "0.48.0" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 613 | dependencies = [ 614 | "windows-targets 0.48.5", 615 | ] 616 | 617 | [[package]] 618 | name = "windows-sys" 619 | version = "0.52.0" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 622 | dependencies = [ 623 | "windows-targets 0.52.6", 624 | ] 625 | 626 | [[package]] 627 | name = "windows-sys" 628 | version = "0.59.0" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 631 | dependencies = [ 632 | "windows-targets 0.52.6", 633 | ] 634 | 635 | [[package]] 636 | name = "windows-targets" 637 | version = "0.48.5" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 640 | dependencies = [ 641 | "windows_aarch64_gnullvm 0.48.5", 642 | "windows_aarch64_msvc 0.48.5", 643 | "windows_i686_gnu 0.48.5", 644 | "windows_i686_msvc 0.48.5", 645 | "windows_x86_64_gnu 0.48.5", 646 | "windows_x86_64_gnullvm 0.48.5", 647 | "windows_x86_64_msvc 0.48.5", 648 | ] 649 | 650 | [[package]] 651 | name = "windows-targets" 652 | version = "0.52.6" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 655 | dependencies = [ 656 | "windows_aarch64_gnullvm 0.52.6", 657 | "windows_aarch64_msvc 0.52.6", 658 | "windows_i686_gnu 0.52.6", 659 | "windows_i686_gnullvm", 660 | "windows_i686_msvc 0.52.6", 661 | "windows_x86_64_gnu 0.52.6", 662 | "windows_x86_64_gnullvm 0.52.6", 663 | "windows_x86_64_msvc 0.52.6", 664 | ] 665 | 666 | [[package]] 667 | name = "windows_aarch64_gnullvm" 668 | version = "0.48.5" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 671 | 672 | [[package]] 673 | name = "windows_aarch64_gnullvm" 674 | version = "0.52.6" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 677 | 678 | [[package]] 679 | name = "windows_aarch64_msvc" 680 | version = "0.48.5" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 683 | 684 | [[package]] 685 | name = "windows_aarch64_msvc" 686 | version = "0.52.6" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 689 | 690 | [[package]] 691 | name = "windows_i686_gnu" 692 | version = "0.48.5" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 695 | 696 | [[package]] 697 | name = "windows_i686_gnu" 698 | version = "0.52.6" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 701 | 702 | [[package]] 703 | name = "windows_i686_gnullvm" 704 | version = "0.52.6" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 707 | 708 | [[package]] 709 | name = "windows_i686_msvc" 710 | version = "0.48.5" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 713 | 714 | [[package]] 715 | name = "windows_i686_msvc" 716 | version = "0.52.6" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 719 | 720 | [[package]] 721 | name = "windows_x86_64_gnu" 722 | version = "0.48.5" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 725 | 726 | [[package]] 727 | name = "windows_x86_64_gnu" 728 | version = "0.52.6" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 731 | 732 | [[package]] 733 | name = "windows_x86_64_gnullvm" 734 | version = "0.48.5" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 737 | 738 | [[package]] 739 | name = "windows_x86_64_gnullvm" 740 | version = "0.52.6" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 743 | 744 | [[package]] 745 | name = "windows_x86_64_msvc" 746 | version = "0.48.5" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 749 | 750 | [[package]] 751 | name = "windows_x86_64_msvc" 752 | version = "0.52.6" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 755 | 756 | [[package]] 757 | name = "winnow" 758 | version = "0.6.20" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" 761 | dependencies = [ 762 | "memchr", 763 | ] 764 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "datatest-stable" 3 | version = "0.3.2" 4 | description = "Data-driven tests that work on stable Rust" 5 | repository = "https://github.com/nextest-rs/datatest-stable" 6 | license = "MIT OR Apache-2.0" 7 | publish = true 8 | readme = "README.md" 9 | edition = "2021" 10 | categories = ["development-tools::testing"] 11 | keywords = ["datatest", "data-driven-tests", "test-harness"] 12 | rust-version = "1.72" 13 | 14 | [badges] 15 | maintenance = { status = "passively-maintained" } 16 | 17 | [package.metadata.cargo-sync-rdme.badge.badges] 18 | maintenance = true 19 | license = true 20 | crates-io = true 21 | docs-rs = true 22 | rust-version = true 23 | 24 | [package.metadata.docs.rs] 25 | all-features = true 26 | rustdoc-args = ["--cfg=doc_cfg"] 27 | 28 | [lints.rust] 29 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(doc_cfg)'] } 30 | 31 | [dependencies] 32 | camino = "1.1.9" 33 | fancy-regex = "0.14.0" 34 | include_dir = { version = "0.7.4", optional = true } 35 | libtest-mimic = "0.8.1" 36 | walkdir = "2.5.0" 37 | 38 | [dev-dependencies] 39 | trybuild = "1.0.105" 40 | 41 | [target.'cfg(unix)'.dev-dependencies] 42 | camino-tempfile = "1.1.1" 43 | fs_extra = "1.3.0" 44 | 45 | [[test]] 46 | name = "example" 47 | harness = false 48 | 49 | [[test]] 50 | name = "integration" 51 | harness = true 52 | 53 | [features] 54 | include-dir = ["dep:include_dir"] 55 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | # Print a help message. 2 | help: 3 | just --list 4 | 5 | 6 | # Run `cargo hack --feature-powerset` with the given arguments. 7 | powerset *args: 8 | cargo hack --feature-powerset {{args}} 9 | 10 | # Build docs for crates and direct dependencies 11 | rustdoc: 12 | @cargo tree --depth 1 -e normal --prefix none --workspace \ 13 | | gawk '{ gsub(" v", "@", $0); printf("%s\n", $1); }' \ 14 | | xargs printf -- '-p %s\n' \ 15 | | RUSTC_BOOTSTRAP=1 RUSTDOCFLAGS='--cfg=doc_cfg' xargs cargo doc --no-deps --lib --all-features 16 | 17 | echo "/ /rustdoc/datatest_stable/ 301" > target/doc/_redirects 18 | 19 | # Generate README.md files using `cargo-sync-rdme`. 20 | generate-readmes: 21 | cargo sync-rdme --toolchain nightly --all-features 22 | 23 | # Collect coverage, pass in `--html` to get an HTML report 24 | coverage *args: 25 | cargo +nightly llvm-cov --no-report nextest --all-features 26 | cargo +nightly llvm-cov --no-report --doc --all-features 27 | cargo +nightly llvm-cov report --doctests {{args}} 28 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # datatest-stable 3 | 4 | 5 | [![Maintenance: passively-maintained](https://img.shields.io/badge/maintenance-passively--maintained-yellowgreen.svg?)](https://doc.rust-lang.org/cargo/reference/manifest.html#the-badges-section) 6 | ![License: MIT OR Apache-2.0](https://img.shields.io/crates/l/datatest-stable.svg?) 7 | [![crates.io](https://img.shields.io/crates/v/datatest-stable.svg?logo=rust)](https://crates.io/crates/datatest-stable) 8 | [![docs.rs](https://img.shields.io/docsrs/datatest-stable.svg?logo=docs.rs)](https://docs.rs/datatest-stable) 9 | [![Rust: ^1.72.0](https://img.shields.io/badge/rust-^1.72.0-93450a.svg?logo=rust)](https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field) 10 | 11 | 12 | `datatest-stable` is a test harness intended to write *file-driven* or *data-driven* tests, 13 | where individual test case fixtures are specified as files and not as code. 14 | 15 | Given: 16 | 17 | * a test `my_test` that accepts a path, and optionally the contents as input 18 | * a directory to look for files (test fixtures) in 19 | * a pattern to match files on 20 | 21 | `datatest-stable` will call the `my_test` function once per matching file in 22 | the directory. Directory traversals are recursive. 23 | 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. 28 | 29 | ## Usage 30 | 31 | 1. Configure the test target by setting `harness = false` in `Cargo.toml`: 32 | 33 | ````toml 34 | [[test]] 35 | name = "" 36 | harness = false 37 | ```` 38 | 39 | 2. Call the `datatest_stable::harness!` macro as: 40 | 41 | ````rust,ignore 42 | datatest_stable::harness! { 43 | { test = my_test, root = "path/to/fixtures", pattern = r".*" }, 44 | } 45 | ```` 46 | 47 | * `test` - The test function to be executed on each matching input. This function can be one 48 | of: 49 | 50 | * `fn(&Path) -> datatest_stable::Result<()>` 51 | * `fn(&Utf8Path) -> datatest_stable::Result<()>` ([`Utf8Path`](https://docs.rs/camino/1.1.9/camino/struct.Utf8Path.html) is part of the 52 | [`camino`](https://docs.rs/camino/1.1.9/camino/index.html) library, and is re-exported here for convenience.) 53 | * `fn(&P, String) -> datatest_stable::Result<()>` where `P` is `Path` or `Utf8Path`. If the 54 | extra `String` parameter is specified, the contents of the file will be loaded and passed in 55 | as a string (erroring out if that failed). 56 | * `fn(&P, Vec) -> datatest_stable::Result<()>` where `P` is `Path` or `Utf8Path`. If the 57 | extra `Vec` parameter is specified, the contents of the file will be loaded and passed 58 | in as a `Vec` (erroring out if that failed). 59 | * `root` - The path to the root directory where the input files (fixtures) 60 | live. Relative paths are resolved relative to the crate root (the directory where the crate’s 61 | `Cargo.toml` is located). 62 | 63 | `root` is an arbitrary expression that implements 64 | [`Display`](https://doc.rust-lang.org/nightly/core/fmt/trait.Display.html), such as `&str`, or a function call that 65 | returns a [`Utf8PathBuf`](https://docs.rs/camino/1.1.9/camino/struct.Utf8PathBuf.html). 66 | 67 | * `pattern` - a regex used to match against and select each file to be tested. Extended regexes 68 | with lookaround and backtracking are supported via the [`fancy_regex`](https://docs.rs/fancy-regex/0.14.0/fancy_regex/index.html) crate. 69 | 70 | `pattern` is an arbitrary expression that implements [`Display`](https://doc.rust-lang.org/nightly/core/fmt/trait.Display.html), such as 71 | `&str`, or a function call that returns a `String`. 72 | 73 | `pattern` is optional, and defaults to `r".*"` (match all files). 74 | 75 | The three parameters can be repeated if you have multiple sets of data-driven tests to be run: 76 | 77 | ````rust,ignore 78 | datatest_stable::harness! { 79 | { test = testfn1, root = root1, pattern = pattern1 }, 80 | { test = testfn2, root = root2 }, 81 | } 82 | ```` 83 | 84 | Trailing commas are optional. 85 | 86 | ### Relative and absolute paths 87 | 88 | The `pattern` argument is tested against the **relative** path of each file, 89 | **excluding** the `root` prefix – not the absolute path. 90 | 91 | The `Path` and `Utf8Path` passed into the test functions are created by 92 | joining `root` to the relative path of the file. 93 | 94 | * If `root` is **relative**, the paths passed in are relative to the crate root. 95 | * If `root` is **absolute**, the paths passed in are absolute. 96 | 97 | For uniformity, all relative paths use `/` as the path separator, 98 | including on Windows, and all absolute paths use the platform’s native 99 | separator throughout. 100 | 101 | ### Examples 102 | 103 | This is an example test. Use it with `harness = false`. 104 | 105 | ````rust 106 | use datatest_stable::Utf8Path; 107 | use std::path::Path; 108 | 109 | fn my_test(path: &Path) -> datatest_stable::Result<()> { 110 | // ... write test here 111 | 112 | Ok(()) 113 | } 114 | 115 | fn my_test_utf8(path: &Utf8Path, contents: String) -> datatest_stable::Result<()> { 116 | // ... write test here 117 | 118 | Ok(()) 119 | } 120 | 121 | datatest_stable::harness! { 122 | { test = my_test, root = "path/to/fixtures" }, 123 | { 124 | test = my_test_utf8, 125 | root = "path/to/fixtures", 126 | pattern = r"^.*\.txt$", 127 | }, 128 | } 129 | ```` 130 | 131 | If `path/to/fixtures` contains a file `foo/bar.txt`, then: 132 | 133 | * The pattern `r"^.*/*"` will match `foo/bar.txt`. 134 | * `my_test` and `my_test_utf8` will be called with `"path/to/fixtures/foo/bar.txt"`. 135 | 136 | ### Embedding directories at compile time 137 | 138 | With the `include-dir` feature enabled, you can use the 139 | [`include_dir`](https://docs.rs/include_dir) crate’s [`include_dir!`](https://docs.rs/include_dir_macros/0.7.4/include_dir_macros/macro.include_dir.html) macro. 140 | This allows you to embed directories into the binary at compile time. 141 | 142 | This is generally not recommended for rapidly-changing test data, since each 143 | change will force a rebuild. But it can be useful for relatively-unchanging 144 | data suites distributed separately, e.g. on crates.io. 145 | 146 | With the `include-dir` feature enabled, you can use: 147 | 148 | ````rust 149 | // The `include_dir!` macro is re-exported for convenience. 150 | use datatest_stable::include_dir; 151 | use std::path::Path; 152 | 153 | fn my_test(path: &Path, contents: Vec) -> datatest_stable::Result<()> { 154 | // ... write test here 155 | Ok(()) 156 | } 157 | 158 | datatest_stable::harness! { 159 | { test = my_test, root = include_dir!("tests/files"), pattern = r"^.*\.json$" }, 160 | } 161 | ```` 162 | 163 | You can also use directories published as `static` items in upstream crates: 164 | 165 | ````rust 166 | use datatest_stable::{include_dir, Utf8Path}; 167 | 168 | // In the upstream crate: 169 | pub static FIXTURES: include_dir::Dir<'static> = 170 | include_dir!("$CARGO_MANIFEST_DIR/tests/files"); 171 | 172 | // In your test: 173 | fn my_test(path: &Utf8Path, contents: String) -> datatest_stable::Result<()> { 174 | // ... write test here 175 | Ok(()) 176 | } 177 | 178 | datatest_stable::harness! { 179 | { test = my_test, root = &FIXTURES }, 180 | } 181 | ```` 182 | 183 | In this case, the passed-in `Path` and `Utf8Path` are always **relative** to 184 | the root of the included directory. Like elsewhere in `datatest-stable`, 185 | these relative paths always use forward slashes as separators, including on 186 | Windows. 187 | 188 | Because the files don’t exist on disk, the test functions must accept their 189 | contents as either a `String` or a `Vec`. If the argument is not 190 | provided, the harness will panic at runtime. 191 | 192 | ### Conditionally embedding directories 193 | 194 | It is also possible to conditionally include directories at compile time via 195 | a feature flag. For example, you might have an internal-only `testing` 196 | feature that you turn on locally, but users don’t on crates.io. In that 197 | case, you can use: 198 | 199 | ````rust 200 | use datatest_stable::Utf8Path; 201 | 202 | // In the library itself: 203 | pub mod fixtures { 204 | #[cfg(feature = "testing")] 205 | pub static FIXTURES: &str = "tests/files"; 206 | 207 | #[cfg(not(feature = "testing"))] 208 | pub static FIXTURES: include_dir::Dir<'static> = 209 | include_dir::include_dir!("$CARGO_MANIFEST_DIR/tests/files"); 210 | } 211 | 212 | // In the test: 213 | fn my_test(path: &Utf8Path, contents: String) -> datatest_stable::Result<()> { 214 | // ... write test here 215 | Ok(()) 216 | } 217 | 218 | datatest_stable::harness! { 219 | { test = my_test, root = &fixtures::FIXTURES, pattern = r"^inputs/.*$" }, 220 | } 221 | ```` 222 | 223 | In this case, note that `path` will be relative to the **crate directory** 224 | (e.g. `tests/files/foo/bar.txt`) if `FIXTURES` is a string, and relative to 225 | the **include directory** (e.g. `foo/bar.txt`) if `FIXTURES` is a 226 | [`Dir`](https://docs.rs/include_dir/0.7.4/include_dir/dir/struct.Dir.html). Your test should be prepared to handle either 227 | case. 228 | 229 | ## Features 230 | 231 | * `include-dir`: Enables the `include_dir!` macro, which allows embedding 232 | directories at compile time. This feature is disabled by default. 233 | 234 | ## Minimum supported Rust version (MSRV) 235 | 236 | The minimum supported Rust version is **Rust 1.72**. MSRV bumps may be accompanied by a minor 237 | version update; at any time, Rust versions from at least the last 6 months are supported. 238 | 239 | ## See also 240 | 241 | * [`datatest`](https://crates.io/crates/datatest): the original inspiration for this crate, with 242 | more features but targeting nightly Rust. 243 | * [Data-driven testing](https://en.wikipedia.org/wiki/Data-driven_testing) 244 | 245 | 246 | ## License 247 | 248 | This project is available under the terms of either the [Apache 2.0 license](LICENSE-APACHE) or the [MIT 249 | license](LICENSE-MIT). 250 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | sign-tag = true 2 | # Required for templates below to work 3 | consolidate-commits = false 4 | pre-release-commit-message = "[{{crate_name}}] version {{version}}" 5 | tag-message = "[{{crate_name}}] version {{version}}" 6 | tag-name = "datatest-stable-{{version}}" 7 | publish = false 8 | dependent-version = "upgrade" 9 | -------------------------------------------------------------------------------- /scripts/cargo-release-publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Use cargo-release to publish crates to crates.io. 4 | 5 | set -xe -o pipefail 6 | 7 | # cargo-release requires a release off a branch (maybe it shouldn't?) 8 | # Check out this branch, creating it if it doesn't exist. 9 | git checkout -B to-release 10 | 11 | # --execute: actually does the release 12 | # --no-confirm: don't ask for confirmation, since this is a non-interactive script 13 | cargo release publish --publish --execute --no-confirm --workspace "$@" 14 | 15 | git checkout - 16 | git branch -D to-release 17 | -------------------------------------------------------------------------------- /scripts/fix-readmes.awk: -------------------------------------------------------------------------------- 1 | # Fix up readmes: 2 | # * Replace ## with # in code blocks. 3 | # * Remove [] without a following () from output. 4 | 5 | BEGIN { 6 | true = 1 7 | false = 0 8 | in_block = false 9 | } 10 | 11 | { 12 | if (!in_block && $0 ~ /^```/) { 13 | in_block = true 14 | } else if (in_block && $0 ~ /^```$/) { 15 | in_block = false 16 | } 17 | 18 | if (in_block) { 19 | sub(/## /, "# ") 20 | print $0 21 | } else { 22 | # Strip [] without a () that immediately follows them from 23 | # the output. 24 | subbed = gensub(/\[([^\[]+)]([^\(]|$)/, "\\1\\2", "g") 25 | print subbed 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/data_source.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) The datatest-stable Contributors 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use camino::{Utf8Component, Utf8Path, Utf8PathBuf}; 5 | 6 | #[derive(Debug)] 7 | #[doc(hidden)] 8 | pub enum DataSource { 9 | // The path has had normalize_slashes applied to it. 10 | Directory(Utf8PathBuf), 11 | #[cfg(feature = "include-dir")] 12 | IncludeDir(std::borrow::Cow<'static, include_dir::Dir<'static>>), 13 | } 14 | 15 | impl DataSource { 16 | /// Iterates over all files in the data source. 17 | /// 18 | /// This returns entries that have just been discovered, so they're expected 19 | /// to exist. 20 | pub(crate) fn walk_files(&self) -> Box> + '_> { 21 | match self { 22 | DataSource::Directory(path) => Box::new(iter_directory(path)), 23 | #[cfg(feature = "include-dir")] 24 | DataSource::IncludeDir(dir) => Box::new(iter_include_dir(dir)), 25 | } 26 | } 27 | 28 | /// Finds a test path from the filter provided. 29 | /// 30 | /// The path might or might not exist -- the caller should call `.exists()` 31 | /// to ensure it does. 32 | /// 33 | /// Used for `--exact` matches. 34 | pub(crate) fn derive_exact(&self, filter: &str, test_name: &str) -> Option { 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 | ); 41 | match self { 42 | DataSource::Directory(path) => Some(TestEntry { 43 | source: TestSource::Path(normalize_slashes(&path.join(&rel_path))), 44 | rel_path, 45 | }), 46 | #[cfg(feature = "include-dir")] 47 | DataSource::IncludeDir(dir) => { 48 | let file = dir.get_file(&rel_path)?; 49 | Some(TestEntry { 50 | source: TestSource::IncludeDir(file), 51 | rel_path, 52 | }) 53 | } 54 | } 55 | } 56 | 57 | /// Returns true if data is not available on disk and must be provided from 58 | /// an in-memory buffer. 59 | pub(crate) fn is_in_memory(&self) -> bool { 60 | match self { 61 | DataSource::Directory(_) => false, 62 | #[cfg(feature = "include-dir")] 63 | DataSource::IncludeDir(_) => true, 64 | } 65 | } 66 | 67 | pub(crate) fn display(&self) -> String { 68 | match self { 69 | DataSource::Directory(path) => format!("directory: `{path}`"), 70 | #[cfg(feature = "include-dir")] 71 | DataSource::IncludeDir(_) => "included directory".to_string(), 72 | } 73 | } 74 | } 75 | 76 | fn iter_directory(root: &Utf8Path) -> impl Iterator> + '_ { 77 | walkdir::WalkDir::new(root) 78 | .into_iter() 79 | .filter(|res| { 80 | // Continue to bubble up all errors to the parent. 81 | res.as_ref().map_or(true, |entry| { 82 | entry.file_type().is_file() 83 | && entry 84 | .file_name() 85 | .to_str() 86 | .is_some_and(|s| !s.starts_with('.')) // Skip hidden files 87 | }) 88 | }) 89 | .map(move |res| match res { 90 | Ok(entry) => { 91 | let path = Utf8PathBuf::try_from(entry.into_path()) 92 | .map_err(|error| error.into_io_error())?; 93 | Ok(TestEntry::from_full_path(root, path)) 94 | } 95 | Err(error) => Err(error.into()), 96 | }) 97 | } 98 | 99 | #[cfg(feature = "include-dir")] 100 | fn iter_include_dir<'a>( 101 | dir: &'a include_dir::Dir<'static>, 102 | ) -> impl Iterator> + 'a { 103 | // Need to maintain a stack to do a depth-first traversal. 104 | struct IncludeDirIter<'a> { 105 | stack: Vec<&'a include_dir::DirEntry<'a>>, 106 | } 107 | 108 | impl<'a> Iterator for IncludeDirIter<'a> { 109 | type Item = &'a include_dir::File<'a>; 110 | 111 | fn next(&mut self) -> Option { 112 | while let Some(entry) = self.stack.pop() { 113 | match entry { 114 | include_dir::DirEntry::File(file) => { 115 | return Some(file); 116 | } 117 | include_dir::DirEntry::Dir(dir) => { 118 | self.stack.extend(dir.entries()); 119 | } 120 | } 121 | } 122 | 123 | None 124 | } 125 | } 126 | 127 | IncludeDirIter { 128 | stack: dir.entries().iter().collect(), 129 | } 130 | .map(|file| { 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 | }; 140 | Ok(TestEntry { 141 | source: TestSource::IncludeDir(file), 142 | rel_path, 143 | }) 144 | }) 145 | } 146 | 147 | #[derive(Debug)] 148 | pub(crate) struct TestEntry { 149 | source: TestSource, 150 | rel_path: Utf8PathBuf, 151 | } 152 | 153 | impl TestEntry { 154 | pub(crate) fn from_full_path(root: &Utf8Path, path: Utf8PathBuf) -> Self { 155 | let path = normalize_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 | })); 160 | Self { 161 | source: TestSource::Path(path), 162 | rel_path, 163 | } 164 | } 165 | 166 | pub(crate) fn derive_test_name(&self, test_name: &str) -> String { 167 | format!("{}::{}", test_name, self.rel_path) 168 | } 169 | 170 | pub(crate) fn read(&self) -> crate::Result> { 171 | match &self.source { 172 | TestSource::Path(path) => std::fs::read(path) 173 | .map_err(|err| format!("error reading file '{path}': {err}").into()), 174 | #[cfg(feature = "include-dir")] 175 | TestSource::IncludeDir(file) => Ok(file.contents().to_vec()), 176 | } 177 | } 178 | 179 | pub(crate) fn read_as_string(&self) -> crate::Result { 180 | match &self.source { 181 | TestSource::Path(path) => std::fs::read_to_string(path) 182 | .map_err(|err| format!("error reading file '{path}' as UTF-8: {err}").into()), 183 | #[cfg(feature = "include-dir")] 184 | TestSource::IncludeDir(file) => { 185 | let contents = file.contents().to_vec(); 186 | String::from_utf8(contents).map_err(|err| { 187 | format!( 188 | "error reading included file at '{}' as UTF-8: {err}", 189 | self.rel_path 190 | ) 191 | .into() 192 | }) 193 | } 194 | } 195 | } 196 | 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. 205 | /// 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. 209 | pub(crate) fn test_path(&self) -> &Utf8Path { 210 | match &self.source { 211 | TestSource::Path(path) => path, 212 | #[cfg(feature = "include-dir")] 213 | TestSource::IncludeDir(_) => { 214 | // The UTF-8-encoded version of file.path is stored in `rel_path`. 215 | &self.rel_path 216 | } 217 | } 218 | } 219 | 220 | /// Returns the path to the file on disk. 221 | /// 222 | /// If the data source is an `include_dir`, this will return `None`. 223 | pub(crate) fn disk_path(&self) -> Option<&Utf8Path> { 224 | match &self.source { 225 | TestSource::Path(path) => Some(path), 226 | #[cfg(feature = "include-dir")] 227 | TestSource::IncludeDir(_) => None, 228 | } 229 | } 230 | 231 | /// Returns true if the path exists. 232 | pub(crate) fn exists(&self) -> bool { 233 | match &self.source { 234 | TestSource::Path(path) => path.exists(), 235 | #[cfg(feature = "include-dir")] 236 | TestSource::IncludeDir(_) => { 237 | // include_dir files are guaranteed to exist. 238 | true 239 | } 240 | } 241 | } 242 | } 243 | 244 | #[cfg(windows)] 245 | #[track_caller] 246 | fn normalize_slashes(path: &Utf8Path) -> Utf8PathBuf { 247 | if is_truly_relative(path) { 248 | rel_path_to_forward_slashes(path) 249 | } else { 250 | path.as_str().replace('/', "\\").into() 251 | } 252 | } 253 | 254 | #[cfg(windows)] 255 | #[track_caller] 256 | fn rel_path_to_forward_slashes(path: &Utf8Path) -> Utf8PathBuf { 257 | assert!(is_truly_relative(path), "path {path} must be relative"); 258 | path.as_str().replace('\\', "/").into() 259 | } 260 | 261 | #[cfg(not(windows))] 262 | #[track_caller] 263 | fn normalize_slashes(path: &Utf8Path) -> Utf8PathBuf { 264 | path.to_owned() 265 | } 266 | 267 | #[cfg(not(windows))] 268 | #[track_caller] 269 | fn rel_path_to_forward_slashes(path: &Utf8Path) -> Utf8PathBuf { 270 | assert!(is_truly_relative(path), "path {path} must be relative"); 271 | path.to_owned() 272 | } 273 | 274 | /// Returns true if this is a path with no root-dir or prefix components. 275 | /// 276 | /// On Windows, unlike `path.is_relative()`, this rejects paths like "C:temp" 277 | /// and "\temp". 278 | #[track_caller] 279 | fn is_truly_relative(path: &Utf8Path) -> bool { 280 | path.components().all(|c| match c { 281 | Utf8Component::Normal(_) | Utf8Component::CurDir | Utf8Component::ParentDir => true, 282 | Utf8Component::RootDir | Utf8Component::Prefix(_) => false, 283 | }) 284 | } 285 | 286 | #[derive(Debug)] 287 | #[doc(hidden)] 288 | pub(crate) enum TestSource { 289 | /// A data source on disk, with the path being the relative path to the file 290 | /// from the crate root. 291 | Path(Utf8PathBuf), 292 | #[cfg(feature = "include-dir")] 293 | IncludeDir(&'static include_dir::File<'static>), 294 | } 295 | 296 | /// Polymorphic dispatch to resolve data sources 297 | /// 298 | /// This is similar to how `test_kinds` works. Here, we're assuming that 299 | /// `include_dir::Dir` will never implement `ToString`. This isn't provable to 300 | /// the compiler directly, but is a reasonable assumption. 301 | /// 302 | /// This could use auto(de)ref specialization to be more semver-safe, but a 303 | /// `Display` impl on `include_dir::Dir` is exceedingly unlikely by Rust 304 | /// community standards, and meanwhile this produces better error messages. 305 | #[doc(hidden)] 306 | pub mod data_source_kinds { 307 | use super::*; 308 | 309 | mod private { 310 | pub trait AsDirectorySealed {} 311 | #[cfg(feature = "include-dir")] 312 | pub trait AsIncludeDirSealed {} 313 | } 314 | 315 | // -- As directory --- 316 | 317 | pub trait AsDirectory: private::AsDirectorySealed { 318 | fn resolve_data_source(self) -> DataSource; 319 | } 320 | 321 | impl private::AsDirectorySealed for T {} 322 | 323 | impl AsDirectory for T { 324 | fn resolve_data_source(self) -> DataSource { 325 | let s = self.to_string(); 326 | let path = Utf8Path::new(&s); 327 | 328 | DataSource::Directory(normalize_slashes(path)) 329 | } 330 | } 331 | 332 | #[cfg(feature = "include-dir")] 333 | pub trait AsIncludeDir: private::AsIncludeDirSealed { 334 | fn resolve_data_source(self) -> DataSource; 335 | } 336 | 337 | #[cfg(feature = "include-dir")] 338 | impl private::AsIncludeDirSealed for include_dir::Dir<'static> {} 339 | 340 | #[cfg(feature = "include-dir")] 341 | impl AsIncludeDir for include_dir::Dir<'static> { 342 | fn resolve_data_source(self) -> DataSource { 343 | DataSource::IncludeDir(std::borrow::Cow::Owned(self)) 344 | } 345 | } 346 | 347 | #[cfg(feature = "include-dir")] 348 | impl private::AsIncludeDirSealed for &'static include_dir::Dir<'static> {} 349 | 350 | #[cfg(feature = "include-dir")] 351 | impl AsIncludeDir for &'static include_dir::Dir<'static> { 352 | fn resolve_data_source(self) -> DataSource { 353 | DataSource::IncludeDir(std::borrow::Cow::Borrowed(self)) 354 | } 355 | } 356 | } 357 | 358 | #[cfg(test)] 359 | mod tests { 360 | use super::*; 361 | 362 | #[test] 363 | fn missing_test_name() { 364 | assert_eq!(derive_test_path("root".into(), "file", "test_name"), None); 365 | } 366 | 367 | #[test] 368 | fn missing_colons() { 369 | assert_eq!( 370 | derive_test_path("root".into(), "test_name", "test_name"), 371 | None 372 | ); 373 | } 374 | 375 | #[test] 376 | fn is_relative_to_root() { 377 | assert_eq!( 378 | derive_test_path("root".into(), "test_name::file", "test_name"), 379 | Some("root/file".into()) 380 | ); 381 | assert_eq!( 382 | derive_test_path("root2".into(), "test_name::file", "test_name"), 383 | Some("root2/file".into()) 384 | ); 385 | } 386 | 387 | #[test] 388 | fn nested_dirs() { 389 | assert_eq!( 390 | derive_test_path("root".into(), "test_name::dir/dir2/file", "test_name"), 391 | Some("root/dir/dir2/file".into()) 392 | ); 393 | } 394 | 395 | #[test] 396 | fn subsequent_module_separators_remain() { 397 | assert_eq!( 398 | derive_test_path("root".into(), "test_name::mod::file", "test_name"), 399 | Some("root/mod::file".into()) 400 | ); 401 | } 402 | 403 | #[test] 404 | fn inverse_of_derive_test_name() { 405 | let root: Utf8PathBuf = "root".into(); 406 | for (path, test_name) in [ 407 | (root.join("foo/bar.txt"), "test_name"), 408 | (root.join("foo::bar.txt"), "test_name"), 409 | (root.join("foo/bar/baz"), "test_name"), 410 | (root.join("foo"), "test_name::mod"), 411 | (root.join("🦀"), "🚀::🚀"), 412 | ] { 413 | let derived_test_name = derive_test_name(&root, &path, test_name); 414 | assert_eq!( 415 | derive_test_path(&root, &derived_test_name, test_name), 416 | Some(path) 417 | ); 418 | } 419 | } 420 | 421 | fn derive_test_name(root: &Utf8Path, path: &Utf8Path, test_name: &str) -> String { 422 | TestEntry::from_full_path(root, path.to_owned()).derive_test_name(test_name) 423 | } 424 | 425 | fn derive_test_path(root: &Utf8Path, path: &str, test_name: &str) -> Option { 426 | DataSource::Directory(root.to_owned()) 427 | .derive_exact(path, test_name) 428 | .map(|entry| entry.test_path().to_owned()) 429 | } 430 | } 431 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) The datatest-stable Contributors 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | #![forbid(unsafe_code)] 5 | 6 | //! `datatest-stable` is a test harness intended to write *file-driven* or *data-driven* tests, 7 | //! where individual test case fixtures are specified as files and not as code. 8 | //! 9 | //! Given: 10 | //! 11 | //! * a test `my_test` that accepts a path, and optionally the contents as input 12 | //! * a directory to look for files (test fixtures) in 13 | //! * a pattern to match files on 14 | //! 15 | //! `datatest-stable` will call the `my_test` function once per matching file in 16 | //! the directory. Directory traversals are recursive. 17 | //! 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. 22 | //! 23 | //! # Usage 24 | //! 25 | //! 1. Configure the test target by setting `harness = false` in `Cargo.toml`: 26 | //! 27 | //! ```toml 28 | //! [[test]] 29 | //! name = "" 30 | //! harness = false 31 | //! ``` 32 | //! 33 | //! 2. Call the `datatest_stable::harness!` macro as: 34 | //! 35 | //! ```rust,ignore 36 | //! datatest_stable::harness! { 37 | //! { test = my_test, root = "path/to/fixtures", pattern = r".*" }, 38 | //! } 39 | //! ``` 40 | //! 41 | //! * `test` - The test function to be executed on each matching input. This function can be one 42 | //! of: 43 | //! * `fn(&Path) -> datatest_stable::Result<()>` 44 | //! * `fn(&Utf8Path) -> datatest_stable::Result<()>` ([`Utf8Path`](camino::Utf8Path) is part of the 45 | //! [`camino`] library, and is re-exported here for convenience.) 46 | //! * `fn(&P, String) -> datatest_stable::Result<()>` where `P` is `Path` or `Utf8Path`. If the 47 | //! extra `String` parameter is specified, the contents of the file will be loaded and passed in 48 | //! as a string (erroring out if that failed). 49 | //! * `fn(&P, Vec) -> datatest_stable::Result<()>` where `P` is `Path` or `Utf8Path`. If the 50 | //! extra `Vec` parameter is specified, the contents of the file will be loaded and passed 51 | //! in as a `Vec` (erroring out if that failed). 52 | //! 53 | //! * `root` - The path to the root directory where the input files (fixtures) 54 | //! live. Relative paths are resolved relative to the crate root (the directory where the crate's 55 | //! `Cargo.toml` is located). 56 | //! 57 | //! `root` is an arbitrary expression that implements 58 | //! [`Display`](std::fmt::Display), such as `&str`, or a function call that 59 | //! returns a [`Utf8PathBuf`](camino::Utf8PathBuf). 60 | //! 61 | //! * `pattern` - a regex used to match against and select each file to be tested. Extended regexes 62 | //! with lookaround and backtracking are supported via the [`fancy_regex`] crate. 63 | //! 64 | //! `pattern` is an arbitrary expression that implements [`Display`](std::fmt::Display), such as 65 | //! `&str`, or a function call that returns a `String`. 66 | //! 67 | //! `pattern` is optional, and defaults to `r".*"` (match all files). 68 | //! 69 | //! The three parameters can be repeated if you have multiple sets of data-driven tests to be run: 70 | //! 71 | //! ```rust,ignore 72 | //! datatest_stable::harness! { 73 | //! { test = testfn1, root = root1, pattern = pattern1 }, 74 | //! { test = testfn2, root = root2 }, 75 | //! } 76 | //! ``` 77 | //! 78 | //! Trailing commas are optional. 79 | //! 80 | //! ## Relative and absolute paths 81 | //! 82 | //! The `pattern` argument is tested against the **relative** path of each file, 83 | //! **excluding** the `root` prefix -- not the absolute path. 84 | //! 85 | //! The `Path` and `Utf8Path` passed into the test functions are created by 86 | //! joining `root` to the relative path of the file. 87 | //! 88 | //! * If `root` is **relative**, the paths passed in are relative to the crate root. 89 | //! * If `root` is **absolute**, the paths passed in are absolute. 90 | //! 91 | //! For uniformity, all relative paths use `/` as the path separator, 92 | //! including on Windows, and all absolute paths use the platform's native 93 | //! separator throughout. 94 | //! 95 | //! ## Examples 96 | //! 97 | //! This is an example test. Use it with `harness = false`. 98 | //! 99 | //! ```rust 100 | //! use datatest_stable::Utf8Path; 101 | //! use std::path::Path; 102 | //! 103 | //! fn my_test(path: &Path) -> datatest_stable::Result<()> { 104 | //! // ... write test here 105 | //! 106 | //! Ok(()) 107 | //! } 108 | //! 109 | //! fn my_test_utf8(path: &Utf8Path, contents: String) -> datatest_stable::Result<()> { 110 | //! // ... write test here 111 | //! 112 | //! Ok(()) 113 | //! } 114 | //! 115 | //! datatest_stable::harness! { 116 | //! { test = my_test, root = "path/to/fixtures" }, 117 | //! { 118 | //! test = my_test_utf8, 119 | //! root = "path/to/fixtures", 120 | //! pattern = r"^.*\.txt$", 121 | //! }, 122 | //! } 123 | //! ``` 124 | //! 125 | //! If `path/to/fixtures` contains a file `foo/bar.txt`, then: 126 | //! 127 | //! * The pattern `r"^.*/*"` will match `foo/bar.txt`. 128 | //! * `my_test` and `my_test_utf8` will be called with `"path/to/fixtures/foo/bar.txt"`. 129 | //! 130 | //! ## Embedding directories at compile time 131 | //! 132 | //! With the `include-dir` feature enabled, you can use the 133 | //! [`include_dir`](https://docs.rs/include_dir) crate's [`include_dir!`] macro. 134 | //! This allows you to embed directories into the binary at compile time. 135 | //! 136 | //! This is generally not recommended for rapidly-changing test data, since each 137 | //! change will force a rebuild. But it can be useful for relatively-unchanging 138 | //! data suites distributed separately, e.g. on crates.io. 139 | //! 140 | //! With the `include-dir` feature enabled, you can use: 141 | //! 142 | #![cfg_attr(feature = "include-dir", doc = "```rust")] 143 | #![cfg_attr(not(feature = "include-dir"), doc = "```rust,ignore")] 144 | //! // The `include_dir!` macro is re-exported for convenience. 145 | //! use datatest_stable::include_dir; 146 | //! use std::path::Path; 147 | //! 148 | //! fn my_test(path: &Path, contents: Vec) -> datatest_stable::Result<()> { 149 | //! // ... write test here 150 | //! Ok(()) 151 | //! } 152 | //! 153 | //! datatest_stable::harness! { 154 | //! { test = my_test, root = include_dir!("tests/files"), pattern = r"^.*\.json$" }, 155 | //! } 156 | //! ``` 157 | //! 158 | //! You can also use directories published as `static` items in upstream crates: 159 | //! 160 | #![cfg_attr(feature = "include-dir", doc = "```rust")] 161 | #![cfg_attr(not(feature = "include-dir"), doc = "```rust,ignore")] 162 | //! use datatest_stable::{include_dir, Utf8Path}; 163 | //! 164 | //! // In the upstream crate: 165 | //! pub static FIXTURES: include_dir::Dir<'static> = 166 | //! include_dir!("$CARGO_MANIFEST_DIR/tests/files"); 167 | //! 168 | //! // In your test: 169 | //! fn my_test(path: &Utf8Path, contents: String) -> datatest_stable::Result<()> { 170 | //! // ... write test here 171 | //! Ok(()) 172 | //! } 173 | //! 174 | //! datatest_stable::harness! { 175 | //! { test = my_test, root = &FIXTURES }, 176 | //! } 177 | //! ``` 178 | //! 179 | //! In this case, the passed-in `Path` and `Utf8Path` are always **relative** to 180 | //! the root of the included directory. Like elsewhere in `datatest-stable`, 181 | //! these relative paths always use forward slashes as separators, including on 182 | //! Windows. 183 | //! 184 | //! Because the files don't exist on disk, the test functions must accept their 185 | //! contents as either a `String` or a `Vec`. If the argument is not 186 | //! provided, the harness will panic at runtime. 187 | //! 188 | //! ## Conditionally embedding directories 189 | //! 190 | //! It is also possible to conditionally include directories at compile time via 191 | //! a feature flag. For example, you might have an internal-only `testing` 192 | //! feature that you turn on locally, but users don't on crates.io. In that 193 | //! case, you can use: 194 | //! 195 | #![cfg_attr(feature = "include-dir", doc = "```rust")] 196 | #![cfg_attr(not(feature = "include-dir"), doc = "```rust,ignore")] 197 | //! use datatest_stable::Utf8Path; 198 | //! 199 | //! // In the library itself: 200 | //! pub mod fixtures { 201 | //! #[cfg(feature = "testing")] 202 | //! pub static FIXTURES: &str = "tests/files"; 203 | //! 204 | //! #[cfg(not(feature = "testing"))] 205 | //! pub static FIXTURES: include_dir::Dir<'static> = 206 | //! include_dir::include_dir!("$CARGO_MANIFEST_DIR/tests/files"); 207 | //! } 208 | //! 209 | //! // In the test: 210 | //! fn my_test(path: &Utf8Path, contents: String) -> datatest_stable::Result<()> { 211 | //! // ... write test here 212 | //! Ok(()) 213 | //! } 214 | //! 215 | //! datatest_stable::harness! { 216 | //! { test = my_test, root = &fixtures::FIXTURES, pattern = r"^inputs/.*$" }, 217 | //! } 218 | //! ``` 219 | //! 220 | //! In this case, note that `path` will be relative to the **crate directory** 221 | //! (e.g. `tests/files/foo/bar.txt`) if `FIXTURES` is a string, and relative to 222 | //! the **include directory** (e.g. `foo/bar.txt`) if `FIXTURES` is a 223 | //! [`Dir`](include_dir::Dir). Your test should be prepared to handle either 224 | //! case. 225 | //! 226 | //! # Features 227 | //! 228 | //! * `include-dir`: Enables the `include_dir!` macro, which allows embedding 229 | //! directories at compile time. This feature is disabled by default. 230 | //! 231 | //! # Minimum supported Rust version (MSRV) 232 | //! 233 | //! The minimum supported Rust version is **Rust 1.72**. MSRV bumps may be accompanied by a minor 234 | //! version update; at any time, Rust versions from at least the last 6 months are supported. 235 | //! 236 | //! # See also 237 | //! 238 | //! * [`datatest`](https://crates.io/crates/datatest): the original inspiration for this crate, with 239 | //! more features but targeting nightly Rust. 240 | //! * [Data-driven testing](https://en.wikipedia.org/wiki/Data-driven_testing) 241 | 242 | #![warn(missing_docs)] 243 | #![cfg_attr(doc_cfg, feature(doc_cfg, doc_auto_cfg))] 244 | 245 | mod data_source; 246 | mod macros; 247 | mod runner; 248 | 249 | /// The result type for `datatest-stable` tests. 250 | pub type Result = std::result::Result>; 251 | 252 | #[doc(hidden)] 253 | pub use self::data_source::{data_source_kinds, DataSource}; 254 | /// Not part of the public API, just used for macros. 255 | #[doc(hidden)] 256 | pub use self::runner::{runner, test_kinds, Requirements, TestFn}; 257 | /// A re-export of this type from the `camino` crate, since it forms part of function signatures. 258 | #[doc(no_inline)] 259 | pub use camino::Utf8Path; 260 | /// A re-export of `include_dir!` from the `include_dir` crate, for convenience. 261 | #[cfg(feature = "include-dir")] 262 | #[doc(no_inline)] 263 | pub use include_dir::include_dir; 264 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) The datatest-stable Contributors 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | /// `datatest-stable` test harness entry point. Should be declared in the test module. 5 | /// 6 | /// Also, `harness` should be set to `false` for that test module in `Cargo.toml` (see [Configuring 7 | /// a target](https://doc.rust-lang.org/cargo/reference/manifest.html#configuring-a-target)). 8 | #[macro_export] 9 | macro_rules! harness { 10 | ( $( { $($args:tt)* } ),+ $(,)* ) => { 11 | fn main() -> ::std::process::ExitCode { 12 | let mut requirements = Vec::new(); 13 | use $crate::data_source_kinds::*; 14 | use $crate::test_kinds::*; 15 | 16 | $( 17 | $crate::harness_collect!(@gather_test requirements, { $($args)*, } => { }); 18 | )+ 19 | 20 | $crate::runner(&requirements) 21 | } 22 | }; 23 | ( $( $name:path, $root:expr, $pattern:expr ),+ $(,)* ) => { 24 | // This is the old format with datatest-stable 0.2. Print a nice message 25 | // in this case. 26 | const _: () = { 27 | compile_error!( 28 | concat!(r"this format is no longer supported -- please switch to specifying as: 29 | 30 | datatest_stable::harness! { 31 | ", 32 | $(concat!(" { test = ", stringify!($name), ", root = ", stringify!($root), ", pattern = ", stringify!($pattern), " },\n"),)+ 33 | r"} 34 | 35 | note: patterns are now evaluated relative to the provided root, not to the crate root 36 | ")); 37 | }; 38 | } 39 | } 40 | 41 | #[macro_export] 42 | #[doc(hidden)] 43 | macro_rules! harness_collect { 44 | // Gather `test` 45 | (@gather_test 46 | $requirements:expr, 47 | // Note: here and below, rest always ends with at least 1 comma 48 | { test = $test:path, $($rest:tt)* } => 49 | { } 50 | ) => { 51 | $crate::harness_collect!(@gather_root 52 | $requirements, 53 | { $($rest)* } => 54 | { test = $test, } 55 | ); 56 | }; 57 | 58 | // `test` not found 59 | (@gather_test 60 | $requirements:expr, 61 | { $key:ident $($rest:tt)* } => 62 | { } 63 | ) => { 64 | compile_error!(concat!("expected `test`, found `", stringify!($key), "`")); 65 | }; 66 | 67 | // No remaining arguments 68 | (@gather_test 69 | $requirements:expr, 70 | { $(,)* } => 71 | { } 72 | ) => { 73 | compile_error!("expected `test`, but ran out of arguments"); 74 | }; 75 | 76 | // Something that isn't an identifier 77 | (@gather_test 78 | $requirements:expr, 79 | { $($rest:tt)* } => 80 | { } 81 | ) => { 82 | compile_error!(concat!("expected `test`, found non-identifier token: (rest: ", stringify!($($rest)*), ")")); 83 | }; 84 | 85 | // Gather `root` 86 | (@gather_root 87 | $requirements:expr, 88 | { root = $root:expr, $($rest:tt)* } => 89 | { $($collected:tt)* } 90 | ) => { 91 | $crate::harness_collect!(@gather_pattern 92 | $requirements, 93 | { $($rest)* } => 94 | { $($collected)* root = $root, } 95 | ); 96 | }; 97 | 98 | // `root` not found 99 | (@gather_root 100 | $requirements:expr, 101 | { $key:ident $($rest:tt)* } => 102 | { $($collected:tt)* } 103 | ) => { 104 | compile_error!(concat!("expected `root`, found `", stringify!($key), "`")); 105 | }; 106 | 107 | // No remaining arguments 108 | (@gather_root 109 | $requirements:expr, 110 | { $(,)* } => 111 | { $($collected:tt)* } 112 | ) => { 113 | compile_error!(concat!("expected `root`, but ran out of arguments (collected: ", stringify!($($collected)*), ")")); 114 | }; 115 | 116 | // Something that isn't an identifier 117 | (@gather_root 118 | $requirements:expr, 119 | { $($rest:tt)* } => 120 | { $($collected:tt)* } 121 | ) => { 122 | compile_error!(concat!("expected `root`, found non-identifier token (rest: ", stringify!($($rest)*), ")")); 123 | }; 124 | 125 | // Gather pattern 126 | (@gather_pattern 127 | $requirements:expr, 128 | { pattern = $pattern:expr, $($rest:tt)* } => 129 | { $($collected:tt)* } 130 | ) => { 131 | $crate::harness_collect!(@finish 132 | $requirements, 133 | { $($rest)* } => 134 | { $($collected)* pattern = $pattern, } 135 | ); 136 | }; 137 | 138 | // `pattern` not found 139 | (@gather_pattern 140 | $requirements:expr, 141 | { $key:ident $($rest:tt)* } => 142 | { $($collected:tt)* } 143 | ) => { 144 | compile_error!(concat!("expected `pattern`, found `", stringify!($key), "`")); 145 | }; 146 | 147 | // `pattern` not found: no remaining arguments 148 | (@gather_pattern 149 | $requirements:expr, 150 | { $(,)* } => 151 | { $($collected:tt)* } 152 | ) => { 153 | $crate::harness_collect!(@finish 154 | $requirements, 155 | { } => 156 | { $($collected)* pattern = ".*", } 157 | ); 158 | }; 159 | 160 | // Something that isn't an identifier 161 | (@gather_pattern 162 | $requirements:expr, 163 | { $($rest:tt)* } => 164 | { $($collected:tt)* } 165 | ) => { 166 | compile_error!(concat!("expected `pattern`, found non-identifier token (rest: ", stringify!($($rest)*), ")")); 167 | }; 168 | 169 | // Finish - no more arguments allowed 170 | (@finish 171 | $requirements:expr, 172 | { $(,)* } => 173 | { test = $test:path, root = $root:expr, pattern = $pattern:expr, } 174 | ) => { 175 | $requirements.push( 176 | $crate::Requirements::new( 177 | $test.kind().resolve($test), 178 | stringify!($test).to_string(), 179 | $root.resolve_data_source(), 180 | $pattern.to_string() 181 | ) 182 | ); 183 | }; 184 | 185 | // Finish - unexpected extra arguments 186 | (@finish 187 | $requirements:expr, 188 | { $($unexpected:tt)+ } => 189 | { $($collected:tt)* } 190 | ) => { 191 | compile_error!(concat!("unexpected extra arguments: ", stringify!($($unexpected)+))); 192 | }; 193 | } 194 | -------------------------------------------------------------------------------- /src/runner.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) The datatest-stable Contributors 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use crate::{data_source::TestEntry, DataSource, Result}; 5 | use camino::{Utf8Path, Utf8PathBuf}; 6 | use libtest_mimic::{Arguments, Trial}; 7 | use std::{path::Path, process::ExitCode}; 8 | 9 | #[doc(hidden)] 10 | pub fn runner(requirements: &[Requirements]) -> ExitCode { 11 | if let Some(cwd) = custom_cwd() { 12 | std::env::set_current_dir(cwd).expect("set custom working directory"); 13 | } 14 | 15 | let args = Arguments::from_args(); 16 | 17 | let tests = find_tests(&args, requirements); 18 | 19 | let conclusion = libtest_mimic::run(&args, tests); 20 | 21 | // This used to use `Conclusion::exit`, but that exits the process via `std::process::exit` as 22 | // of libtest-mimic 0.7.0. This breaks some things, e.g. llvm-cov on Windows. 23 | // https://github.com/nextest-rs/datatest-stable/issues/20 24 | // 25 | // Use `std::process::ExitCode` instead, and return it in main. 26 | 27 | conclusion.exit_code() 28 | } 29 | 30 | /// One of our tests requires that a custom working directory be set. This function is used to do 31 | /// that. 32 | fn custom_cwd() -> Option { 33 | std::env::var("__DATATEST_CWD").ok().map(Utf8PathBuf::from) 34 | } 35 | 36 | fn find_tests(args: &Arguments, requirements: &[Requirements]) -> Vec { 37 | let tests: Vec<_> = if let Some(exact_filter) = exact_filter(args) { 38 | let exact_tests: Vec<_> = requirements 39 | .iter() 40 | .filter_map(|req| req.exact(exact_filter)) 41 | .collect(); 42 | 43 | match NextestKind::determine() { 44 | NextestKind::InUse { process_per_test } => { 45 | if exact_tests.is_empty() { 46 | panic!("Failed to find exact match for filter {exact_filter}"); 47 | } 48 | if process_per_test && exact_tests.len() > 1 { 49 | panic!( 50 | "Only expected one but found {} exact matches for filter {exact_filter}", 51 | exact_tests.len() 52 | ); 53 | } 54 | } 55 | NextestKind::NotInUse => {} 56 | } 57 | 58 | exact_tests 59 | } else if is_full_scan_forbidden(args) { 60 | panic!("Exact filter was expected to be used"); 61 | } else { 62 | let mut tests: Vec<_> = requirements.iter().flat_map(|req| req.expand()).collect(); 63 | tests.sort_unstable_by(|a, b| a.name().cmp(b.name())); 64 | tests 65 | }; 66 | tests 67 | } 68 | 69 | #[derive(Clone, Copy, Debug)] 70 | enum NextestKind { 71 | NotInUse, 72 | InUse { process_per_test: bool }, 73 | } 74 | 75 | impl NextestKind { 76 | fn determine() -> Self { 77 | if std::env::var("NEXTEST").as_deref() == Ok("1") { 78 | // Process-per-test means that exactly one test should be run. 79 | let process_per_test = 80 | std::env::var("NEXTEST_EXECUTION_MODE").as_deref() == Ok("process-per-test"); 81 | Self::InUse { process_per_test } 82 | } else { 83 | Self::NotInUse 84 | } 85 | } 86 | } 87 | 88 | fn is_full_scan_forbidden(args: &Arguments) -> bool { 89 | !args.list && std::env::var("__DATATEST_FULL_SCAN_FORBIDDEN").as_deref() == Ok("1") 90 | } 91 | 92 | fn exact_filter(args: &Arguments) -> Option<&str> { 93 | if args.exact && args.skip.is_empty() { 94 | args.filter.as_deref() 95 | } else { 96 | None 97 | } 98 | } 99 | 100 | #[doc(hidden)] 101 | pub struct Requirements { 102 | test: TestFn, 103 | test_name: String, 104 | root: DataSource, 105 | pattern: String, 106 | } 107 | 108 | impl Requirements { 109 | #[doc(hidden)] 110 | pub fn new(test: TestFn, test_name: String, root: DataSource, pattern: String) -> Self { 111 | // include_dir data sources aren't compatible with test functions that 112 | // don't accept the contents as an argument. 113 | if !test.loads_data() && root.is_in_memory() { 114 | panic!( 115 | "test data for '{}' is stored in memory, so it \ 116 | must accept file contents as an argument", 117 | test_name 118 | ); 119 | } 120 | 121 | Self { 122 | test, 123 | test_name, 124 | root, 125 | pattern, 126 | } 127 | } 128 | 129 | fn trial(&self, entry: TestEntry) -> Trial { 130 | let testfn = self.test; 131 | let name = entry.derive_test_name(&self.test_name); 132 | Trial::test(name, move || { 133 | testfn 134 | .call(entry) 135 | .map_err(|err| format!("{:?}", err).into()) 136 | }) 137 | } 138 | 139 | fn exact(&self, filter: &str) -> Option { 140 | let entry = self.root.derive_exact(filter, &self.test_name)?; 141 | entry.exists().then(|| self.trial(entry)) 142 | } 143 | 144 | /// Scans all files in a given directory, finds matching ones and generates a test descriptor 145 | /// for each of them. 146 | fn expand(&self) -> Vec { 147 | let re = fancy_regex::Regex::new(&self.pattern) 148 | .unwrap_or_else(|_| panic!("invalid regular expression: '{}'", self.pattern)); 149 | 150 | let tests: Vec<_> = self 151 | .root 152 | .walk_files() 153 | .filter_map(|entry_res| { 154 | let entry = entry_res.expect("error reading directory"); 155 | let path_str = entry.match_path().as_str(); 156 | if re.is_match(path_str).unwrap_or_else(|error| { 157 | panic!( 158 | "error matching pattern '{}' against path '{}' : {}", 159 | self.pattern, path_str, error 160 | ) 161 | }) { 162 | Some(self.trial(entry)) 163 | } else { 164 | None 165 | } 166 | }) 167 | .collect(); 168 | 169 | // We want to avoid silent fails due to typos in regexp! 170 | if tests.is_empty() { 171 | panic!( 172 | "no test cases found for test '{}' -- scanned {} with pattern '{}'", 173 | self.test_name, 174 | self.root.display(), 175 | self.pattern, 176 | ); 177 | } 178 | 179 | tests 180 | } 181 | } 182 | 183 | // -- Polymorphic dispatch -- 184 | 185 | #[derive(Clone, Copy)] 186 | #[doc(hidden)] 187 | pub enum TestFn { 188 | // Functions that work on paths. 189 | Base(TestFnBase), 190 | /// Test functions that load a file as a string (UTF-8 text). 191 | LoadString(TestFnLoadString), 192 | /// Test functions that load a file as binary data. 193 | LoadBinary(TestFnLoadBinary), 194 | } 195 | 196 | impl TestFn { 197 | fn loads_data(&self) -> bool { 198 | match self { 199 | TestFn::Base(_) => false, 200 | TestFn::LoadString(_) | TestFn::LoadBinary(_) => true, 201 | } 202 | } 203 | 204 | fn call(&self, entry: TestEntry) -> Result<()> { 205 | match self { 206 | TestFn::Base(f) => { 207 | let path = entry 208 | .disk_path() 209 | .expect("test entry being on disk was checked in the constructor"); 210 | f.call(path) 211 | } 212 | TestFn::LoadString(f) => f.call(entry), 213 | TestFn::LoadBinary(f) => f.call(entry), 214 | } 215 | } 216 | } 217 | 218 | #[derive(Clone, Copy)] 219 | #[doc(hidden)] 220 | pub enum TestFnBase { 221 | Path(fn(&Path) -> Result<()>), 222 | Utf8Path(fn(&Utf8Path) -> Result<()>), 223 | } 224 | 225 | impl TestFnBase { 226 | fn call(&self, path: &Utf8Path) -> Result<()> { 227 | match self { 228 | TestFnBase::Path(f) => f(path.as_ref()), 229 | TestFnBase::Utf8Path(f) => f(path), 230 | } 231 | } 232 | } 233 | 234 | #[derive(Clone, Copy)] 235 | #[doc(hidden)] 236 | pub enum TestFnLoadString { 237 | Path(fn(&Path, String) -> Result<()>), 238 | Utf8Path(fn(&Utf8Path, String) -> Result<()>), 239 | } 240 | 241 | impl TestFnLoadString { 242 | fn call(&self, entry: TestEntry) -> Result<()> { 243 | let contents = entry.read_as_string()?; 244 | match self { 245 | TestFnLoadString::Path(f) => f(entry.test_path().as_ref(), contents), 246 | TestFnLoadString::Utf8Path(f) => f(entry.test_path(), contents), 247 | } 248 | } 249 | } 250 | 251 | #[derive(Clone, Copy)] 252 | #[doc(hidden)] 253 | pub enum TestFnLoadBinary { 254 | Path(fn(&Path, Vec) -> Result<()>), 255 | Utf8Path(fn(&Utf8Path, Vec) -> Result<()>), 256 | } 257 | 258 | impl TestFnLoadBinary { 259 | fn call(&self, entry: TestEntry) -> Result<()> { 260 | let contents = entry.read()?; 261 | match self { 262 | TestFnLoadBinary::Path(f) => f(entry.test_path().as_ref(), contents), 263 | TestFnLoadBinary::Utf8Path(f) => f(entry.test_path(), contents), 264 | } 265 | } 266 | } 267 | 268 | /// Implementations to allow `TestFn` to be created with functions of one of several types. 269 | /// 270 | /// datatest-stable supports several options for the shape of test functions. This code allows: 271 | /// 272 | /// * the `harness!` macro to accept any of these types of functions, generating the same syntactic 273 | /// code for each. 274 | /// * for all of those functions to resolve to a single `TestFn` type which can do dynamic dispatch. 275 | /// 276 | /// There are several challenges to overcome here, the main one being that you cannot have multiple 277 | /// different kinds of `Fn`s impl the same trait. For example, rustc will not accept this code 278 | /// ([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=007c814fb457bd4e95d0073745b5f8d9)): 279 | /// 280 | /// ```rust,ignore 281 | /// trait MyTrait {} 282 | /// 283 | /// impl MyTrait for F {} 284 | /// impl MyTrait for F {} 285 | /// ``` 286 | /// 287 | /// This means that it is not possible to write code that is type-system generic over different 288 | /// kinds of function types. 289 | /// 290 | /// But since `harness!` is a macro, it can expand to code that's syntactically the same for each of 291 | /// those function types, but semantically resolves to completely different types. That's exactly 292 | /// what we do here. 293 | /// 294 | /// This is a trick very similar to autoref specialization, though we don't use the automatic `&` 295 | /// Rust inserts while dereferencing methods. See [autoref-specialization]. 296 | /// 297 | /// # Notes 298 | /// 299 | /// We can't say `impl PathKind for fn(&Path) -> Result<()>` because Rust won't automatically coerce 300 | /// the concrete function type to the function pointer. (If we could, then we wouldn't need to rely 301 | /// on the macro-ness of `harness!` at all, and could just have a single trait implemented for all 302 | /// the different function pointer types.) To address this, we use a two-step process. 303 | /// 304 | /// * Step 1: Implement `PathKind` for all `F: Fn(&Path) -> Result<()>`. This allows a `.kind()` 305 | /// method to exist which returns a new `PathTag` type. 306 | /// * Step 2: Implement `PathTag::new` which only takes `fn(&Path) -> Result<()>`. *This* type can 307 | /// coerce to the function pointer, which can be stored in the `TestFn` enum. 308 | /// 309 | /// This two-step process is similar to the one documented in [autoref-specialization]. 310 | /// 311 | /// [autoref-specialization]: https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md 312 | #[doc(hidden)] 313 | pub mod test_kinds { 314 | 315 | use super::*; 316 | 317 | mod private { 318 | // We need to define a separate Sealed for each of the tags below, because Rust doesn't allow 319 | // multiple kinds of F: Fn(T) -> Result<()> to implement the same trait. 320 | pub trait PathSealed {} 321 | pub trait Utf8PathSealed {} 322 | pub trait PathStringSealed {} 323 | pub trait Utf8PathStringSealed {} 324 | pub trait PathBytesSealed {} 325 | pub trait Utf8PathBytesSealed {} 326 | } 327 | 328 | // -- Paths -- 329 | 330 | #[doc(hidden)] 331 | pub struct PathTag; 332 | 333 | impl PathTag { 334 | #[inline] 335 | pub fn resolve(self, f: fn(&Path) -> Result<()>) -> TestFn { 336 | TestFn::Base(TestFnBase::Path(f)) 337 | } 338 | } 339 | 340 | #[doc(hidden)] 341 | pub trait PathKind: private::PathSealed { 342 | #[inline] 343 | fn kind(&self) -> PathTag { 344 | PathTag 345 | } 346 | } 347 | 348 | impl Result<()>> private::PathSealed for F {} 349 | impl Result<()>> PathKind for F {} 350 | 351 | // -- UTF-8 paths -- 352 | 353 | #[doc(hidden)] 354 | pub struct Utf8PathTag; 355 | 356 | impl Utf8PathTag { 357 | #[inline] 358 | pub fn resolve(&self, f: fn(&Utf8Path) -> Result<()>) -> TestFn { 359 | TestFn::Base(TestFnBase::Utf8Path(f)) 360 | } 361 | } 362 | 363 | #[doc(hidden)] 364 | pub trait Utf8PathKind: private::Utf8PathSealed { 365 | #[inline] 366 | fn kind(&self) -> Utf8PathTag { 367 | Utf8PathTag 368 | } 369 | } 370 | 371 | impl Result<()>> private::Utf8PathSealed for F {} 372 | impl Result<()>> Utf8PathKind for F {} 373 | 374 | // -- Path, load file as string -- 375 | 376 | #[doc(hidden)] 377 | pub struct PathStringTag; 378 | 379 | impl PathStringTag { 380 | #[inline] 381 | pub fn resolve(self, f: fn(&Path, String) -> Result<()>) -> TestFn { 382 | TestFn::LoadString(TestFnLoadString::Path(f)) 383 | } 384 | } 385 | 386 | #[doc(hidden)] 387 | pub trait PathStringKind: private::PathStringSealed { 388 | #[inline] 389 | fn kind(&self) -> PathStringTag { 390 | PathStringTag 391 | } 392 | } 393 | 394 | impl Result<()>> private::PathStringSealed for F {} 395 | impl Result<()>> PathStringKind for F {} 396 | 397 | // -- Utf8Path, load file as string -- 398 | 399 | #[doc(hidden)] 400 | pub struct Utf8PathStringTag; 401 | 402 | impl Utf8PathStringTag { 403 | #[inline] 404 | pub fn resolve(self, f: fn(&Utf8Path, String) -> Result<()>) -> TestFn { 405 | TestFn::LoadString(TestFnLoadString::Utf8Path(f)) 406 | } 407 | } 408 | 409 | #[doc(hidden)] 410 | pub trait Utf8PathStringKind: private::Utf8PathStringSealed { 411 | #[inline] 412 | fn kind(&self) -> Utf8PathStringTag { 413 | Utf8PathStringTag 414 | } 415 | } 416 | 417 | impl Result<()>> private::Utf8PathStringSealed for F {} 418 | impl Result<()>> Utf8PathStringKind for F {} 419 | 420 | // -- Path, load file as binary -- 421 | 422 | #[doc(hidden)] 423 | pub struct PathBytesTag; 424 | 425 | impl PathBytesTag { 426 | #[inline] 427 | pub fn resolve(self, f: fn(&Path, Vec) -> Result<()>) -> TestFn { 428 | TestFn::LoadBinary(TestFnLoadBinary::Path(f)) 429 | } 430 | } 431 | 432 | #[doc(hidden)] 433 | pub trait PathBytesKind: private::PathBytesSealed { 434 | #[inline] 435 | fn kind(&self) -> PathBytesTag { 436 | PathBytesTag 437 | } 438 | } 439 | 440 | impl) -> Result<()>> private::PathBytesSealed for F {} 441 | impl) -> Result<()>> PathBytesKind for F {} 442 | 443 | // -- Utf8Path, load file as binary -- 444 | 445 | #[doc(hidden)] 446 | pub struct Utf8PathBytesTag; 447 | 448 | impl Utf8PathBytesTag { 449 | #[inline] 450 | pub fn resolve(self, f: fn(&Utf8Path, Vec) -> Result<()>) -> TestFn { 451 | TestFn::LoadBinary(TestFnLoadBinary::Utf8Path(f)) 452 | } 453 | } 454 | 455 | #[doc(hidden)] 456 | pub trait Utf8PathBytesKind: private::Utf8PathBytesSealed { 457 | #[inline] 458 | fn kind(&self) -> Utf8PathBytesTag { 459 | Utf8PathBytesTag 460 | } 461 | } 462 | 463 | impl) -> Result<()>> private::Utf8PathBytesSealed for F {} 464 | impl) -> Result<()>> Utf8PathBytesKind for F {} 465 | } 466 | 467 | #[cfg(all(test, feature = "include-dir"))] 468 | mod include_dir_tests { 469 | use super::*; 470 | use std::borrow::Cow; 471 | 472 | #[test] 473 | #[should_panic = "test data for 'my_test' is stored in memory, \ 474 | so it must accept file contents as an argument"] 475 | fn include_dir_without_arg() { 476 | fn my_test(_: &Path) -> Result<()> { 477 | Ok(()) 478 | } 479 | 480 | Requirements::new( 481 | TestFn::Base(TestFnBase::Path(my_test)), 482 | "my_test".to_owned(), 483 | DataSource::IncludeDir(Cow::Owned(include_dir::include_dir!("tests/files"))), 484 | "xxx".to_owned(), 485 | ); 486 | } 487 | } 488 | -------------------------------------------------------------------------------- /tests/compile-fail/empty-arg-list.rs: -------------------------------------------------------------------------------- 1 | datatest_stable::harness! { 2 | { } 3 | } 4 | -------------------------------------------------------------------------------- /tests/compile-fail/empty-arg-list.stderr: -------------------------------------------------------------------------------- 1 | error: expected `test`, but ran out of arguments 2 | --> tests/compile-fail/empty-arg-list.rs:1:1 3 | | 4 | 1 | / datatest_stable::harness! { 5 | 2 | | { } 6 | 3 | | } 7 | | |_^ 8 | | 9 | = note: this error originates in the macro `$crate::harness_collect` which comes from the expansion of the macro `datatest_stable::harness` (in Nightly builds, run with -Z macro-backtrace for more info) 10 | -------------------------------------------------------------------------------- /tests/compile-fail/extra-args.rs: -------------------------------------------------------------------------------- 1 | datatest_stable::harness! { 2 | { test = my_test, root = "abc", pattern = ".*", extra = "xyz" } 3 | } 4 | -------------------------------------------------------------------------------- /tests/compile-fail/extra-args.stderr: -------------------------------------------------------------------------------- 1 | error: unexpected extra arguments: extra = "xyz", 2 | --> tests/compile-fail/extra-args.rs:1:1 3 | | 4 | 1 | / datatest_stable::harness! { 5 | 2 | | { test = my_test, root = "abc", pattern = ".*", extra = "xyz" } 6 | 3 | | } 7 | | |_^ 8 | | 9 | = note: this error originates in the macro `$crate::harness_collect` which comes from the expansion of the macro `datatest_stable::harness` (in Nightly builds, run with -Z macro-backtrace for more info) 10 | -------------------------------------------------------------------------------- /tests/compile-fail/incorrect-arg-in-pattern.rs: -------------------------------------------------------------------------------- 1 | datatest_stable::harness! { 2 | { test = my_test, root = "tests/files", foo = "bar", } 3 | } 4 | -------------------------------------------------------------------------------- /tests/compile-fail/incorrect-arg-in-pattern.stderr: -------------------------------------------------------------------------------- 1 | error: expected `pattern`, found `foo` 2 | --> tests/compile-fail/incorrect-arg-in-pattern.rs:1:1 3 | | 4 | 1 | / datatest_stable::harness! { 5 | 2 | | { test = my_test, root = "tests/files", foo = "bar", } 6 | 3 | | } 7 | | |_^ 8 | | 9 | = note: this error originates in the macro `$crate::harness_collect` which comes from the expansion of the macro `datatest_stable::harness` (in Nightly builds, run with -Z macro-backtrace for more info) 10 | -------------------------------------------------------------------------------- /tests/compile-fail/missing-root-comma.rs: -------------------------------------------------------------------------------- 1 | datatest_stable::harness! { 2 | { 3 | test = my_test, 4 | pattern = r"^.*(? tests/compile-fail/missing-root-comma.rs:1:1 3 | | 4 | 1 | / datatest_stable::harness! { 5 | 2 | | { 6 | 3 | | test = my_test, 7 | 4 | | pattern = r"^.*(? tests/compile-fail/missing-root-no-args.rs:1:1 3 | | 4 | 1 | / datatest_stable::harness! { 5 | 2 | | { 6 | 3 | | test = my_test, 7 | 4 | | }, 8 | 5 | | } 9 | | |_^ 10 | | 11 | = note: this error originates in the macro `$crate::harness_collect` which comes from the expansion of the macro `datatest_stable::harness` (in Nightly builds, run with -Z macro-backtrace for more info) 12 | -------------------------------------------------------------------------------- /tests/compile-fail/missing-root-no-comma.rs: -------------------------------------------------------------------------------- 1 | datatest_stable::harness! { 2 | { test = my_test, pattern = r"^.*(? tests/compile-fail/missing-root-no-comma.rs:1:1 3 | | 4 | 1 | / datatest_stable::harness! { 5 | 2 | | { test = my_test, pattern = r"^.*(? tests/compile-fail/missing-test-comma.rs:1:1 3 | | 4 | 1 | / datatest_stable::harness! { 5 | 2 | | { 6 | 3 | | root = "tests/files", 7 | 4 | | pattern = r"^.*(? tests/compile-fail/missing-test-no-comma.rs:1:1 3 | | 4 | 1 | / datatest_stable::harness! { 5 | 2 | | { root = "tests/files", test = my_test, pattern = r"^.*(? tests/compile-fail/old-format.rs:1:1 10 | | 11 | 1 | datatest_stable::harness!(foo, "path1", r"^.*/*$", bar::baz, "path2", r"^.*/*$"); 12 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 13 | | 14 | = note: this error originates in the macro `datatest_stable::harness` (in Nightly builds, run with -Z macro-backtrace for more info) 15 | -------------------------------------------------------------------------------- /tests/compile-fail/pattern-not-ident.rs: -------------------------------------------------------------------------------- 1 | datatest_stable::harness! { 2 | { test = my_test, root = "abc", "xyz" = foo } 3 | } 4 | -------------------------------------------------------------------------------- /tests/compile-fail/pattern-not-ident.stderr: -------------------------------------------------------------------------------- 1 | error: expected `pattern`, found non-identifier token (rest: "xyz" = foo,) 2 | --> tests/compile-fail/pattern-not-ident.rs:1:1 3 | | 4 | 1 | / datatest_stable::harness! { 5 | 2 | | { test = my_test, root = "abc", "xyz" = foo } 6 | 3 | | } 7 | | |_^ 8 | | 9 | = note: this error originates in the macro `$crate::harness_collect` which comes from the expansion of the macro `datatest_stable::harness` (in Nightly builds, run with -Z macro-backtrace for more info) 10 | -------------------------------------------------------------------------------- /tests/compile-fail/root-not-ident.rs: -------------------------------------------------------------------------------- 1 | datatest_stable::harness! { 2 | { test = my_test, "xyz" = foo } 3 | } 4 | -------------------------------------------------------------------------------- /tests/compile-fail/root-not-ident.stderr: -------------------------------------------------------------------------------- 1 | error: expected `root`, found non-identifier token (rest: "xyz" = foo,) 2 | --> tests/compile-fail/root-not-ident.rs:1:1 3 | | 4 | 1 | / datatest_stable::harness! { 5 | 2 | | { test = my_test, "xyz" = foo } 6 | 3 | | } 7 | | |_^ 8 | | 9 | = note: this error originates in the macro `$crate::harness_collect` which comes from the expansion of the macro `datatest_stable::harness` (in Nightly builds, run with -Z macro-backtrace for more info) 10 | -------------------------------------------------------------------------------- /tests/compile-fail/root-out-of-order.rs: -------------------------------------------------------------------------------- 1 | datatest_stable::harness! { 2 | { test = my_test, pattern = r"^.*(? tests/compile-fail/root-out-of-order.rs:1:1 3 | | 4 | 1 | / datatest_stable::harness! { 5 | 2 | | { test = my_test, pattern = r"^.*(? tests/compile-fail/test-not-ident.rs:1:1 3 | | 4 | 1 | / datatest_stable::harness! { 5 | 2 | | { "xyz" = foo } 6 | 3 | | } 7 | | |_^ 8 | | 9 | = note: this error originates in the macro `$crate::harness_collect` which comes from the expansion of the macro `datatest_stable::harness` (in Nightly builds, run with -Z macro-backtrace for more info) 10 | -------------------------------------------------------------------------------- /tests/compile-fail/test-out-of-order.rs: -------------------------------------------------------------------------------- 1 | datatest_stable::harness! { 2 | { root = "tests/files", pattern = r"^.*(? tests/compile-fail/test-out-of-order.rs:1:1 3 | | 4 | 1 | / datatest_stable::harness! { 5 | 2 | | { root = "tests/files", pattern = r"^.*(? Result<()> { 9 | let mut file = File::open(path)?; 10 | let mut contents = String::new(); 11 | file.read_to_string(&mut contents)?; 12 | 13 | Ok(()) 14 | } 15 | 16 | fn test_artifact_utf8(path: &Utf8Path) -> Result<()> { 17 | test_artifact(path.as_ref()) 18 | } 19 | 20 | fn test_artifact_utf8_abs(path: &Utf8Path) -> Result<()> { 21 | // path must be absolute 22 | assert!(path.is_absolute(), "path must be absolute: {:?}", path); 23 | 24 | // On Windows, the path must be normalized to use backslashes. 25 | #[cfg(windows)] 26 | { 27 | assert!( 28 | !path.as_str().contains('/'), 29 | "path must not contain forward slashes: {:?}", 30 | path 31 | ); 32 | } 33 | 34 | test_artifact(path.as_ref()) 35 | } 36 | 37 | #[cfg(feature = "include-dir")] 38 | #[macro_use] 39 | mod with_contents { 40 | use super::*; 41 | 42 | /// Returns an `include_dir::Dir` instance. 43 | macro_rules! maybe_include_dir { 44 | () => { 45 | include_dir::include_dir!("$CARGO_MANIFEST_DIR/tests/files") 46 | }; 47 | } 48 | 49 | /// A `&'static include_dir::Dir` instance. 50 | pub(crate) static MAYBE_INCLUDE_STATIC: include_dir::Dir = 51 | include_dir::include_dir!("$CARGO_MANIFEST_DIR/tests/files"); 52 | 53 | pub(crate) fn test_artifact_string(path: &Path, contents: String) -> Result<()> { 54 | compare_contents(path, contents.as_bytes()) 55 | } 56 | 57 | pub(crate) fn test_artifact_utf8_string(path: &Utf8Path, contents: String) -> Result<()> { 58 | compare_contents(path.as_std_path(), contents.as_bytes()) 59 | } 60 | 61 | pub(crate) fn test_artifact_bytes(path: &Path, contents: Vec) -> Result<()> { 62 | compare_contents(path, &contents) 63 | } 64 | 65 | pub(crate) fn test_artifact_utf8_bytes(path: &Utf8Path, contents: Vec) -> Result<()> { 66 | compare_contents(path.as_std_path(), &contents) 67 | } 68 | 69 | fn compare_contents(path: &Path, expected: &[u8]) -> Result<()> { 70 | // The path must not begin with "tests/files". 71 | assert!( 72 | !path.to_string_lossy().starts_with("tests/files"), 73 | "path must not start with 'tests/files': {:?}", 74 | path 75 | ); 76 | 77 | // Prepend tests/files to the path to get the expected contents. In 78 | // general we can't verify the contents, but in this case we can do so 79 | // because the paths are also available on disk. 80 | let path = format!("tests/files/{}", path.to_str().unwrap()); 81 | compare(path.as_ref(), expected) 82 | } 83 | } 84 | 85 | #[cfg(not(feature = "include-dir"))] 86 | #[macro_use] 87 | mod with_contents { 88 | use super::*; 89 | 90 | /// Returns an `include_dir::Dir` instance. 91 | macro_rules! maybe_include_dir { 92 | () => { 93 | "tests/files" 94 | }; 95 | } 96 | 97 | /// A `&'static include_dir::Dir` instance. 98 | pub(crate) static MAYBE_INCLUDE_STATIC: &str = "tests/files"; 99 | 100 | pub(crate) fn test_artifact_string(path: &Path, contents: String) -> Result<()> { 101 | compare_contents(path, contents.as_bytes()) 102 | } 103 | 104 | pub(crate) fn test_artifact_utf8_string(path: &Utf8Path, contents: String) -> Result<()> { 105 | compare_contents(path.as_std_path(), contents.as_bytes()) 106 | } 107 | 108 | pub(crate) fn test_artifact_bytes(path: &Path, contents: Vec) -> Result<()> { 109 | compare_contents(path, &contents) 110 | } 111 | 112 | pub(crate) fn test_artifact_utf8_bytes(path: &Utf8Path, contents: Vec) -> Result<()> { 113 | compare_contents(path.as_std_path(), &contents) 114 | } 115 | 116 | fn compare_contents(path: &Path, expected: &[u8]) -> Result<()> { 117 | // The path must begin with "tests/files". 118 | assert!( 119 | path.to_string_lossy().starts_with("tests/files"), 120 | "path must start with 'tests/files': {:?}", 121 | path 122 | ); 123 | compare(&path, expected) 124 | } 125 | } 126 | 127 | fn compare(path: &Path, expected: &[u8]) -> Result<()> { 128 | // The path must be relative. 129 | assert!(path.is_relative(), "path must be relative: {:?}", path); 130 | 131 | // The path must not have any backslashes on Windows. 132 | assert!( 133 | !path.to_string_lossy().contains('\\'), 134 | "path must not contain backslashes: {:?}", 135 | path 136 | ); 137 | 138 | let actual = 139 | std::fs::read(path).map_err(|error| format!("failed to read file: {:?}: {error}", path))?; 140 | 141 | assert_eq!(expected, &actual, "file contents match for {:?}", path); 142 | 143 | Ok(()) 144 | } 145 | 146 | #[cfg(windows)] 147 | static TESTS_FILES_MAIN_SEP: &str = "tests\\files"; 148 | 149 | #[cfg(not(windows))] 150 | static TESTS_FILES_MAIN_SEP: &str = "tests/files"; 151 | 152 | fn tests_files_abs() -> String { 153 | std::env::current_dir() 154 | .expect("current dir obtained") 155 | .join("tests/files") 156 | .to_string_lossy() 157 | .into_owned() 158 | } 159 | 160 | datatest_stable::harness! { 161 | { 162 | test = test_artifact, 163 | root = "tests/files", 164 | // This regex pattern skips .skip.txt files. 165 | pattern = r"^.*(? String { 157 | std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()) 158 | } 159 | --------------------------------------------------------------------------------