├── tests ├── test-cases │ ├── case-01.input.txt │ ├── case-02.input.txt │ ├── case-01.output.txt │ ├── case-02.output.txt │ ├── case-03.input-linked.txt │ └── case-03.output-linked.txt ├── cases.txt ├── strings.yaml ├── tests.yaml ├── datatest_stable_unsafe.rs ├── nested.rs ├── datatest.rs ├── datatest_stable.rs ├── unicode.rs ├── bench.rs └── tests │ └── mod.rs ├── src ├── test-cases │ ├── A.input.txt │ └── A.output.txt ├── interceptor.rs ├── data.rs ├── files.rs ├── lib.rs └── runner.rs ├── .gitignore ├── DEV.md ├── ci ├── job-rustfmt.yml ├── job-check.yml ├── job-test.yml └── steps-install-rust.yml ├── datatest-derive ├── Cargo.toml └── src │ └── lib.rs ├── publish.sh ├── LICENSE-MIT ├── README.tpl ├── Cargo.toml ├── .github └── workflows │ └── rust.yml ├── CODE_OF_CONDUCT.md ├── README.md └── LICENSE-APACHE /tests/test-cases/case-01.input.txt: -------------------------------------------------------------------------------- 1 | Kylie -------------------------------------------------------------------------------- /tests/test-cases/case-02.input.txt: -------------------------------------------------------------------------------- 1 | Rahid -------------------------------------------------------------------------------- /src/test-cases/A.input.txt: -------------------------------------------------------------------------------- 1 | A varying CASE StRiNg! -------------------------------------------------------------------------------- /tests/test-cases/case-01.output.txt: -------------------------------------------------------------------------------- 1 | Hello, Kylie! -------------------------------------------------------------------------------- /tests/test-cases/case-02.output.txt: -------------------------------------------------------------------------------- 1 | Hello, Rahid! -------------------------------------------------------------------------------- /src/test-cases/A.output.txt: -------------------------------------------------------------------------------- 1 | a varying case string! -------------------------------------------------------------------------------- /tests/test-cases/case-03.input-linked.txt: -------------------------------------------------------------------------------- 1 | ./case-01.input.txt -------------------------------------------------------------------------------- /tests/test-cases/case-03.output-linked.txt: -------------------------------------------------------------------------------- 1 | ./case-01.output.txt -------------------------------------------------------------------------------- /tests/cases.txt: -------------------------------------------------------------------------------- 1 | Pino 2 | Hello, Pino! 3 | Daria 4 | Hello, Daria! -------------------------------------------------------------------------------- /tests/strings.yaml: -------------------------------------------------------------------------------- 1 | - "firstfirst" 2 | - "secondsecond" 3 | - "thirdthird" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /target 3 | /datatest-derive/target 4 | **/*.rs.bk 5 | Cargo.lock 6 | -------------------------------------------------------------------------------- /tests/tests.yaml: -------------------------------------------------------------------------------- 1 | - name: Pino 2 | expected: Hi, Pino! 3 | - name: Re-L 4 | expected: Hi, Re-L! 5 | - name: Vincent 6 | expected: Hi, Vincent! -------------------------------------------------------------------------------- /DEV.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | Working on this project requires [`cargo-make`](https://crates.io/crates/cargo-make) tool to be installed. Run 4 | `cargo install --force cargo-make` to install one. 5 | 6 | ## Running tests 7 | 8 | Run `cargo test --all` 9 | 10 | ## Releasing a new version 11 | 12 | Run `./publish.sh` -------------------------------------------------------------------------------- /tests/datatest_stable_unsafe.rs: -------------------------------------------------------------------------------- 1 | //! cargo +stable test --features subvert_stable_guarantees,unsafe_test_runner 2 | 3 | #![cfg(feature = "rustc_is_stable")] 4 | #![cfg(feature = "unsafe_test_runner")] 5 | 6 | // We want to share tests between "nightly" and "stable" suites. These have to be two different 7 | // suites as we set `harness = false` for the "stable" one. 8 | include!("tests/mod.rs"); 9 | -------------------------------------------------------------------------------- /ci/job-rustfmt.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | toolchain: stable 3 | jobs: 4 | - job: rustfmt 5 | pool: 6 | vmImage: ubuntu-16.04 7 | steps: 8 | - template: steps-install-rust.yml 9 | parameters: 10 | toolchain: ${{ parameters.toolchain }} 11 | components: 12 | - rustfmt 13 | - script: | 14 | cargo fmt -- --check 15 | displayName: cargo fmt -- --check 16 | 17 | -------------------------------------------------------------------------------- /datatest-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "datatest-derive" 3 | version = "0.8.0" 4 | authors = ["Ivan Dubrov "] 5 | edition = "2018" 6 | repository = "https://github.com/commure/datatest" 7 | license = "MIT/Apache-2.0" 8 | description = """ 9 | Procmacro for the datatest crate 10 | """ 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | quote = "1.0.33" 17 | syn = { version = "2.0.37", features = ["full"] } 18 | proc-macro2 = "1.0.67" 19 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DIR=`dirname $0` 4 | 5 | if [[ -z "${VER}" ]] ; then 6 | echo "Set VER variable to the version to release!" 7 | exit 1 8 | fi 9 | 10 | cargo install cargo-readme 11 | 12 | pushd "${DIR}" 13 | cargo clean 14 | cargo readme --output README.md 15 | cargo test --all 16 | 17 | git tag --annotate --message "releasing version ${VER}" "v${VER}" 18 | git push --tags 19 | popd 20 | 21 | pushd "${DIR}/datatest-derive" 22 | cargo publish 23 | popd 24 | 25 | pushd "${DIR}" 26 | cargo publish 27 | popd 28 | 29 | -------------------------------------------------------------------------------- /ci/job-check.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | toolchain: stable 3 | features: [] 4 | jobs: 5 | - job: check 6 | pool: 7 | vmImage: ubuntu-16.04 8 | steps: 9 | - template: steps-install-rust.yml 10 | parameters: 11 | toolchain: ${{ parameters.toolchain }} 12 | components: 13 | - clippy 14 | - script: | 15 | cargo check --all --all-targets --features "${{ join(' ', parameters.features) }}" 16 | displayName: cargo check 17 | - script: | 18 | cargo clippy --all --features "${{ join(' ', parameters.features) }}" 19 | displayName: cargo clippy --all 20 | 21 | -------------------------------------------------------------------------------- /tests/nested.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(feature = "rustc_is_nightly"))] 2 | #![feature(custom_test_frameworks)] 3 | #![test_runner(datatest::runner)] 4 | 5 | // Make sure we can run tests 6 | 7 | mod inner { 8 | mod another { 9 | use serde::Deserialize; 10 | 11 | #[derive(Deserialize)] 12 | struct GreeterTestCase { 13 | name: String, 14 | expected: String, 15 | } 16 | 17 | #[datatest::data("tests/tests.yaml")] 18 | #[test] 19 | fn data_test_line_only(data: &GreeterTestCase) { 20 | assert_eq!(data.expected, format!("Hi, {}!", data.name)); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/datatest.rs: -------------------------------------------------------------------------------- 1 | //! cargo +nightly test # test_case_registration enabled 2 | //! cargo +nightly test --no-default-features # no test_case_registration, uses ctor 3 | 4 | // self-testing config only: 5 | #![cfg(all(feature = "rustc_is_nightly"))] 6 | #![feature(custom_test_frameworks)] 7 | #![test_runner(datatest::runner)] 8 | 9 | // We want to share tests between "nightly" and "stable" suites. These have to be two different 10 | // suites as we set `harness = false` for the "stable" one. 11 | include!("tests/mod.rs"); 12 | 13 | // Regular tests still work 14 | 15 | #[test] 16 | fn regular_test() { 17 | println!("regular tests also work!"); 18 | } 19 | 20 | #[test] 21 | fn regular_test_result() -> Result<(), Box> { 22 | println!("regular tests also work!"); 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /ci/job-test.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | toolchain: stable 3 | name: test 4 | steps: [] 5 | features: [] 6 | jobs: 7 | - job: ${{ parameters.name }} 8 | displayName: test (${{parameters.toolchain}}) 9 | strategy: 10 | matrix: 11 | Linux: 12 | vmImage: ubuntu-16.04 13 | MacOS: 14 | vmImage: macOS-10.13 15 | Windows: 16 | vmImage: vs2017-win2016 17 | pool: 18 | vmImage: $(vmImage) 19 | 20 | steps: 21 | - template: steps-install-rust.yml 22 | parameters: 23 | toolchain: ${{ parameters.toolchain }} 24 | - script: | 25 | cargo test --all --all-targets --features "${{ join(' ', parameters.features) }}" 26 | displayName: cargo test --all --all-targets 27 | - ${{ each step in parameters.steps }}: 28 | - ${{ each pair in step }}: 29 | ${{ pair.key }}: ${{ pair.value }} 30 | -------------------------------------------------------------------------------- /tests/datatest_stable.rs: -------------------------------------------------------------------------------- 1 | //! cargo +stable test --features subvert_stable_guarantees 2 | 3 | // This test suite is configured with `harness = false` in Cargo.toml. 4 | // So we need to make sure it has a main function when testing nightly 5 | #[cfg(not(feature = "rustc_is_stable"))] 6 | fn main() {} 7 | // And uses the datatest harness when testing stable 8 | #[cfg(feature = "rustc_is_stable")] 9 | datatest::harness!(); 10 | 11 | #[cfg(feature = "rustc_is_stable")] 12 | mod stable { 13 | // Regular test have to use `datatest` variant of `#[test]` to work. 14 | use datatest::test; 15 | 16 | // We want to share tests between "rustc_is_nightly" and "rustc_is_stable" suites. These have to be two different 17 | // suites as we set `harness = false` for the "stable" one. 18 | include!("tests/mod.rs"); 19 | 20 | #[test] 21 | fn regular_test() { 22 | println!("regular tests also work!"); 23 | } 24 | 25 | #[test] 26 | fn regular_test_result() -> Result<(), Box> { 27 | println!("regular tests also work!"); 28 | Ok(()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/unicode.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "rustc_is_nightly")] 2 | #![feature(custom_test_frameworks)] 3 | #![test_runner(datatest::runner)] 4 | 5 | use serde::Deserialize; 6 | use std::fmt; 7 | 8 | #[datatest::files("tests/test-cases", { 9 | // Pattern is defined via `in` operator. Every file from the `directory` above will be matched 10 | // against this regular expression and every matched file will produce a separate test. 11 | input in r"^(.*)\.input\.txt", 12 | // Template defines a rule for deriving dependent file name based on captures of the pattern. 13 | output = r"${1}.output.txt", 14 | })] 15 | #[test] 16 | fn files_testsome_unicode_привет(input: &str, output: &str) { 17 | assert_eq!(format!("Hello, {}!", input), output); 18 | } 19 | 20 | /// This test case item implements [`std::fmt::Display`], which is used to generate test name 21 | #[derive(Deserialize)] 22 | struct GreeterTestCaseNamed { 23 | name: String, 24 | expected: String, 25 | } 26 | 27 | impl fmt::Display for GreeterTestCaseNamed { 28 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 29 | f.write_str(&self.name) 30 | } 31 | } 32 | 33 | #[datatest::data("tests/tests.yaml")] 34 | #[test] 35 | fn data_test_with_some_unicode_привет(data: &GreeterTestCaseNamed) { 36 | assert_eq!(data.expected, format!("Hi, {}!", data.name)); 37 | } 38 | -------------------------------------------------------------------------------- /ci/steps-install-rust.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | toolchain: stable 3 | steps: 4 | # Linux and macOS 5 | - script: | 6 | set -e 7 | curl https://sh.rustup.rs -sSf | sh -s -- -y 8 | echo "##vso[task.setvariable variable=PATH;]$PATH:$HOME/.cargo/bin" 9 | env: 10 | RUSTUP_TOOLCHAIN: ${{parameters.toolchain}} 11 | displayName: "Install rust (*nix)" 12 | condition: not(eq(variables['Agent.OS'], 'Windows_NT')) 13 | # Windows 14 | - script: | 15 | curl -sSf -o rustup-init.exe https://win.rustup.rs 16 | rustup-init.exe -y 17 | set PATH=%PATH%;%USERPROFILE%\.cargo\bin 18 | echo "##vso[task.setvariable variable=PATH;]%PATH%;%USERPROFILE%\.cargo\bin" 19 | env: 20 | RUSTUP_TOOLCHAIN: ${{parameters.toolchain}} 21 | displayName: "Install rust (Windows)" 22 | condition: eq(variables['Agent.OS'], 'Windows_NT') 23 | - script: | 24 | rustup toolchain install ${{parameters.toolchain}} 25 | rustup default ${{parameters.toolchain}} 26 | displayName: rustup default ${{parameters.toolchain}} 27 | - ${{ each component in parameters.components }}: 28 | - script: rustup component add ${{ component }} 29 | displayName: rustup component add ${{ component }} 30 | - script: | 31 | rustup -V 32 | rustup component list --installed 33 | rustc -Vv 34 | cargo -V 35 | displayName: rust versions 36 | -------------------------------------------------------------------------------- /README.tpl: -------------------------------------------------------------------------------- 1 | # Datatest: data-driven tests in Rust 2 | 3 | [![crates.io][Crate Logo]][Crate] 4 | [![Documentation][Doc Logo]][Doc] 5 | [![Build Status][CI Logo]][CI] 6 | 7 | {{readme}} 8 | 9 | # Notes on Rust channel 10 | 11 | Currently this crate targets primarily nightly Rust and can break at any time. 12 | 13 | It could be compiled on stable by enabling certain feature (see `Cargo.toml`), but using this feature would subvert 14 | any stability guarantees Rust provides. 15 | 16 | ## License 17 | 18 | Licensed under either of 19 | 20 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 21 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 22 | 23 | at your option. 24 | 25 | ### Contribution 26 | 27 | Unless you explicitly state otherwise, any contribution intentionally submitted 28 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any 29 | additional terms or conditions. 30 | 31 | 32 | [Crate]: https://crates.io/crates/datatest 33 | [Crate Logo]: https://img.shields.io/crates/v/datatest.svg 34 | 35 | [Doc]: https://docs.rs/datatest 36 | [Doc Logo]: https://docs.rs/datatest/badge.svg 37 | 38 | [CI]: https://dev.azure.com/commure/datatest/_build/latest?definitionId=3&branchName=master 39 | [CI Logo]: https://dev.azure.com/commure/datatest/_apis/build/status/commure.datatest?branchName=master -------------------------------------------------------------------------------- /src/interceptor.rs: -------------------------------------------------------------------------------- 1 | use crate::runner::TestDescriptor; 2 | use std::sync::Once; 3 | 4 | static INTERCEPTOR: Once = Once::new(); 5 | 6 | pub fn install_interceptor() { 7 | INTERCEPTOR.call_once(|| { 8 | intercept_test_main_static(); 9 | }); 10 | } 11 | 12 | fn intercept_test_main_static() { 13 | let ptr = rustc_test::test_main_static as *mut (); 14 | let target = test_main_static_intercepted as *const (); 15 | // 5 is the size of jmp instruction + offset 16 | let diff = isize::wrapping_sub(target as _, ptr as _) as i32 - 5; 17 | 18 | // Patch the test_main_static function so it instead jumps to our own function. 19 | // e9 00 00 00 00 jmp 5 <_main+0x5> 20 | unsafe { 21 | let diff_bytes = diff.to_le_bytes(); 22 | let bytes = ptr as *mut u8; 23 | let result = region::protect_with_handle(bytes, 5, region::Protection::WRITE_EXECUTE); 24 | let _handle = match result { 25 | Ok(h) => h, 26 | Err(err) => { 27 | panic!("Failed to set memory protection, {:?}", err); 28 | } 29 | }; 30 | 31 | std::ptr::write(bytes.offset(0), 0xe9); 32 | std::ptr::write(bytes.offset(1), diff_bytes[0]); 33 | std::ptr::write(bytes.offset(2), diff_bytes[1]); 34 | std::ptr::write(bytes.offset(3), diff_bytes[2]); 35 | std::ptr::write(bytes.offset(4), diff_bytes[3]); 36 | } 37 | } 38 | 39 | fn test_main_static_intercepted(tests: &[&rustc_test::TestDescAndFn]) { 40 | let tests = tests 41 | .iter() 42 | .map(|v| *v as &dyn TestDescriptor) 43 | .collect::>(); 44 | crate::runner(&tests); 45 | } 46 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "datatest" 3 | version = "0.8.0" 4 | authors = ["Ivan Dubrov ", "Giovanny Gutierrez "] 5 | edition = "2018" 6 | repository = "https://github.com/commure/datatest" 7 | license = "MIT/Apache-2.0" 8 | readme = "README.md" 9 | description = """ 10 | Data-driven tests in Rust 11 | """ 12 | 13 | [[test]] 14 | name = "datatest_stable" 15 | harness = false 16 | 17 | [dependencies] 18 | datatest-derive = { path = "datatest-derive", version = "= 0.8.0"} 19 | regex = "1.9.5" 20 | walkdir = "2.4.0" 21 | serde = "1.0.188" 22 | serde_yaml = "0.9.21" 23 | yaml-rust = "0.4.5" 24 | ctor = "0.2.5" 25 | region = { version = "3.0.0", optional = true } 26 | 27 | [dev-dependencies] 28 | serde = { version = "1.0.188", features = ["derive"] } 29 | 30 | [build-dependencies] 31 | version_check = "0.9.3" 32 | 33 | [workspace] 34 | members = [ 35 | "datatest-derive" 36 | ] 37 | 38 | [features] 39 | 40 | # Use `#[test_case]`-based test registration. If this is disabled, datatest will use the `ctor` crate to create a 41 | # global list of all tests. 42 | # 43 | # Enabled by default, only takes effect on nightly and only works when custom_test_frameworks feature is enabled. 44 | test_case_registration = [] 45 | 46 | # Use very, very, very sketchy way of intercepting a test runner on a stable Rust. Without that feature, there are two 47 | # options: 48 | # 1. use `#![test_runner(datatest::runner)]` (nightly-only) 49 | # 2. use `harness = false` in `Cargo.toml` with `datatest::harness!()` macro; however, in this case care must be taken 50 | # to use `#[datatest::test]` instead of regular `#[test]` or otherwise tests will be silently ignored. 51 | unsafe_test_runner = ["region"] 52 | 53 | # Make this crate useable on stable Rust channel. Uses forbidden technique to allow usage of this crate on a stable 54 | # Rust compiler. This, however, does not bring any guarantees above "nightly" -- this crate can break at any time. 55 | subvert_stable_guarantees = [] 56 | 57 | default = ["test_case_registration"] 58 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | # Note: `toolchain` doesn't work for clippy-check (adds `+stable`/`+nightly` to the wrong place), so instead we install 2 | # toolchains as default 3 | on: [pull_request] 4 | name: Rust 5 | env: 6 | RUSTC_BOOTSTRAP: datatest 7 | jobs: 8 | rustfmt: 9 | name: Verify formatting 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout sources 13 | uses: actions/checkout@v1 14 | 15 | - name: Install stable toolchain 16 | uses: actions-rs/toolchain@v1 17 | with: 18 | profile: minimal 19 | toolchain: stable 20 | default: true 21 | components: rustfmt 22 | 23 | - name: Cargo fmt 24 | uses: actions-rs/cargo@v1 25 | with: 26 | command: fmt 27 | args: --all -- --check 28 | 29 | verify: 30 | name: Verify Clippy and Tests 31 | strategy: 32 | matrix: 33 | os: 34 | - ubuntu-latest 35 | - windows-latest 36 | - macos-latest 37 | runs-on: ${{ matrix.os }} 38 | steps: 39 | - name: Checkout sources 40 | uses: actions/checkout@v1 41 | 42 | - name: Cache cargo registry 43 | uses: actions/cache@v1 44 | with: 45 | path: ~/.cargo/registry 46 | key: ${{ runner.os }}-cargo-registry 47 | 48 | - name: Cache cargo index 49 | uses: actions/cache@v1 50 | with: 51 | path: ~/.cargo/git 52 | key: ${{ runner.os }}-cargo-index 53 | 54 | - name: Cache cargo build 55 | uses: actions/cache@v1 56 | with: 57 | path: target 58 | key: ${{ runner.os }}-cargo-build-target 59 | - name: Install stable toolchain 60 | uses: actions-rs/toolchain@v1 61 | with: 62 | profile: minimal 63 | toolchain: 1.72.0 64 | default: true 65 | components: clippy 66 | 67 | - name: Cargo clippy (stable) 68 | uses: actions-rs/clippy-check@v1 69 | with: 70 | token: ${{ secrets.GITHUB_TOKEN }} 71 | args: --all-features --features subvert_stable_guarantees 72 | 73 | - name: Cargo test (stable) 74 | uses: actions-rs/cargo@v1 75 | with: 76 | command: test 77 | args: --all --all-targets --features subvert_stable_guarantees 78 | 79 | - name: Cargo clean (stable) 80 | uses: actions-rs/cargo@v1 81 | with: 82 | command: clean 83 | 84 | - name: Install nightly toolchain 85 | uses: actions-rs/toolchain@v1 86 | with: 87 | profile: minimal 88 | toolchain: nightly-2023-09-28 89 | default: true 90 | components: clippy 91 | 92 | - name: Cargo clippy (nightly) 93 | uses: actions-rs/clippy-check@v1 94 | with: 95 | token: ${{ secrets.GITHUB_TOKEN }} 96 | args: --all-features 97 | 98 | - name: Cargo test (nightly) 99 | uses: actions-rs/cargo@v1 100 | with: 101 | command: test 102 | args: --all --all-targets 103 | -------------------------------------------------------------------------------- /tests/bench.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(feature = "rustc_is_nightly"))] 2 | #![feature(custom_test_frameworks)] 3 | #![test_runner(datatest::runner)] 4 | #![feature(test)] 5 | extern crate test; 6 | 7 | use serde::Deserialize; 8 | use test::Bencher; 9 | 10 | /// File-driven tests are defined via `#[files(...)]` attribute. 11 | /// 12 | /// The first argument to the attribute is the path to the test data (relative to the crate root 13 | /// directory). 14 | /// 15 | /// The second argument is a block of mappings, each mapping defines the rules of deriving test 16 | /// function arguments. 17 | /// 18 | /// Exactly one mapping should be a "pattern" mapping, defined as ` in ""`. `` 19 | /// is a regular expression applied to every file found in the test directory. For each file path 20 | /// matching the regular expression, test runner will create a new test instance. 21 | /// 22 | /// Other mappings are "template" mappings, they define the template to use for deriving the file 23 | /// paths. Each template have a syntax of a [replacement string] from [`regex`] crate. 24 | /// 25 | /// [replacement string]: https://docs.rs/regex/*/regex/struct.Regex.html#method.replace 26 | /// [regex]: https://docs.rs/regex/*/regex/ 27 | #[datatest::files("tests/test-cases", { 28 | // Pattern is defined via `in` operator. Every file from the `directory` above will be matched 29 | // against this regular expression and every matched file will produce a separate test. 30 | input in r"^(.*)\.input\.txt", 31 | // Template defines a rule for deriving dependent file name based on captures of the pattern. 32 | output = r"${1}.output.txt", 33 | })] 34 | #[bench] 35 | fn files_test_strings(bencher: &mut Bencher, input: &str, output: &str) { 36 | bencher.iter(|| { 37 | assert_eq!(format!("Hello, {}!", input), output); 38 | }); 39 | } 40 | 41 | /// Regular tests are also allowed! 42 | #[bench] 43 | fn simple_test(bencher: &mut Bencher) { 44 | bencher.iter(|| { 45 | let palindrome = "never odd or even".replace(' ', ""); 46 | let reversed = palindrome.chars().rev().collect::(); 47 | 48 | assert_eq!(palindrome, reversed) 49 | }) 50 | } 51 | 52 | /// This test case item does not implement [`std::fmt::Display`], so only line number is shown in 53 | /// the test name. 54 | #[derive(Deserialize, Clone)] 55 | struct GreeterTestCase { 56 | name: String, 57 | expected: String, 58 | } 59 | 60 | /// Data-driven tests are defined via `#[datatest::data(..)]` attribute. 61 | /// 62 | /// This attribute specifies a test file with test cases. Currently, the test file have to be in 63 | /// YAML format. This file is deserialized into `Vec`, where `T` is the type of the test function 64 | /// argument (which must implement `serde::Deserialize`). Then, for each element of the vector, a 65 | /// separate test instance is created and executed. 66 | /// 67 | /// Name of each test is derived from the test function module path, test case line number and, 68 | /// optionall, from the [`ToString`] implementation of the test case data (if either [`ToString`] 69 | /// or [`std::fmt::Display`] is implemented). 70 | #[datatest::data("tests/tests.yaml")] 71 | #[bench] 72 | fn data_test_line_only(bencher: &mut Bencher, data: &GreeterTestCase) { 73 | bencher.iter(|| { 74 | assert_eq!(data.expected, format!("Hi, {}!", data.name)); 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | 78 | -------------------------------------------------------------------------------- /src/data.rs: -------------------------------------------------------------------------------- 1 | //! Support module for `#[datatest::data(..)]` 2 | use rustc_test::Bencher; 3 | #[cfg(feature = "rustc_test_TDynBenchFn")] 4 | use rustc_test::TDynBenchFn; 5 | use serde::de::DeserializeOwned; 6 | use std::path::Path; 7 | use yaml_rust::parser::Event; 8 | use yaml_rust::scanner::Marker; 9 | 10 | /// Descriptor used internally for `#[datatest::data(..)]` tests. 11 | #[doc(hidden)] 12 | pub struct DataTestDesc { 13 | pub name: &'static str, 14 | pub ignore: bool, 15 | pub describefn: fn() -> Vec>, 16 | pub source_file: &'static str, 17 | } 18 | 19 | /// Used internally for `#[datatest::data(..)]` tests. 20 | #[doc(hidden)] 21 | pub enum DataTestFn { 22 | TestFn(Box), 23 | 24 | #[cfg(feature = "rustc_test_TDynBenchFn")] 25 | BenchFn(Box), 26 | 27 | #[cfg(not(feature = "rustc_test_TDynBenchFn"))] 28 | BenchFn(Box), 29 | } 30 | 31 | /// Descriptor of the data test case where the type of the test case data is `T`. 32 | pub struct DataTestCaseDesc { 33 | pub case: T, 34 | pub name: Option, 35 | pub location: String, 36 | } 37 | 38 | pub fn yaml( 39 | path: &str, 40 | ) -> Vec> { 41 | let input = std::fs::read_to_string(Path::new(path)) 42 | .unwrap_or_else(|_| panic!("cannot read file '{}'", path)); 43 | 44 | let index = index_cases(&input); 45 | let cases: Vec = serde_yaml::from_str(&input).unwrap(); 46 | assert_eq!(index.len(), cases.len(), "index does not match test cases"); 47 | 48 | index 49 | .into_iter() 50 | .zip(cases) 51 | .map(|(marker, case)| DataTestCaseDesc { 52 | name: TestNameWithDefault::name(&case), 53 | case, 54 | location: format!("line {}", marker.line()), 55 | }) 56 | .collect() 57 | } 58 | 59 | /// Trait abstracting two scenarios: test case implementing [`ToString`] and test case not 60 | /// implementing [`ToString`]. 61 | #[doc(hidden)] 62 | pub trait TestNameWithDefault { 63 | fn name(&self) -> Option; 64 | } 65 | 66 | // For those types which do not implement `ToString`/`Display`. 67 | impl TestNameWithDefault for T { 68 | default fn name(&self) -> Option { 69 | None 70 | } 71 | } 72 | 73 | // For those types which implement `ToString`/`Display`. 74 | impl TestNameWithDefault for T { 75 | fn name(&self) -> Option { 76 | Some(self.to_string()) 77 | } 78 | } 79 | 80 | #[doc(hidden)] 81 | pub struct DataBenchFn(pub fn(&mut Bencher, T), pub T) 82 | where 83 | T: Send + Clone; 84 | 85 | #[cfg(feature = "rustc_test_TDynBenchFn")] 86 | impl rustc_test::TDynBenchFn for DataBenchFn 87 | where 88 | T: Send + Clone, 89 | { 90 | fn run(&self, harness: &mut Bencher) { 91 | (self.0)(harness, self.1.clone()) 92 | } 93 | } 94 | 95 | impl<'r, T> Fn<(&'r mut Bencher,)> for DataBenchFn 96 | where 97 | T: Send + Clone, 98 | { 99 | extern "rust-call" fn call(&self, (bencher,): (&'r mut Bencher,)) { 100 | (self.0)(bencher, self.1.clone()); 101 | } 102 | } 103 | 104 | impl<'r, T> FnOnce<(&'r mut Bencher,)> for DataBenchFn 105 | where 106 | T: Send + Clone, 107 | { 108 | type Output = (); 109 | extern "rust-call" fn call_once(self, harness: (&'r mut Bencher,)) { 110 | (self.0)(harness.0, self.1.clone()) 111 | } 112 | } 113 | 114 | impl<'r, T> FnMut<(&'r mut Bencher,)> for DataBenchFn 115 | where 116 | T: Send + Clone, 117 | { 118 | extern "rust-call" fn call_mut(&mut self, harness: (&'r mut Bencher,)) { 119 | (self.0)(harness.0, self.1.clone()) 120 | } 121 | } 122 | 123 | /// Build an index from the YAML source to the location of each test case (top level array elements). 124 | fn index_cases(source: &str) -> Vec { 125 | let mut parser = yaml_rust::parser::Parser::new(source.chars()); 126 | let mut index = Vec::new(); 127 | let mut depth = 0; 128 | loop { 129 | let (event, marker) = parser.next().expect("invalid YAML"); 130 | match event { 131 | Event::StreamEnd => { 132 | break; 133 | } 134 | Event::Scalar(_, _, _, _) if depth == 1 => { 135 | index.push(marker); 136 | } 137 | Event::MappingStart(_idx) if depth == 1 => { 138 | index.push(marker); 139 | depth += 1; 140 | } 141 | Event::MappingStart(_idx) | Event::SequenceStart(_idx) => { 142 | depth += 1; 143 | } 144 | Event::MappingEnd | Event::SequenceEnd => { 145 | depth -= 1; 146 | } 147 | _ => {} 148 | } 149 | } 150 | 151 | index 152 | } 153 | -------------------------------------------------------------------------------- /src/files.rs: -------------------------------------------------------------------------------- 1 | //! Support module for `#[datatest::files(..)]` 2 | use rustc_test::Bencher; 3 | use std::borrow::Borrow; 4 | use std::path::{Path, PathBuf}; 5 | 6 | /// Used internally for `#[datatest::files(..)]` tests to distinguish regular tests versus benchmark 7 | /// tests. 8 | #[doc(hidden)] 9 | pub enum FilesTestFn { 10 | TestFn(fn(&[PathBuf])), 11 | BenchFn(fn(&mut Bencher, &[PathBuf])), 12 | } 13 | 14 | /// Descriptor used internally for `#[datatest::files(..)]` tests. 15 | #[doc(hidden)] 16 | pub struct FilesTestDesc { 17 | pub name: &'static str, 18 | pub ignore: bool, 19 | pub root: &'static str, 20 | pub params: &'static [&'static str], 21 | pub pattern: usize, 22 | pub ignorefn: Option bool>, 23 | pub testfn: FilesTestFn, 24 | pub source_file: &'static str, 25 | } 26 | 27 | /// Trait defining conversion into a function argument. We use it to convert discovered paths 28 | /// to test data (captured as `&Path`) into what is expected by the function. 29 | /// 30 | /// Why so complex? We need a way to convert an argument received as an element of slice `&[PathBuf]` 31 | /// and convert it into what the function expects. 32 | /// 33 | /// The difficulty here is that for owned arguments we can create value and just pass it down to the 34 | /// function. However, for arguments taking slices, we need to store value somewhere on the stack 35 | /// and pass a reference. 36 | /// 37 | /// Theoretically, our proc macro could generate a different piece of code to handle that, but 38 | /// to avoid the complexity in proc macro, it always generates code in the form of: 39 | /// 40 | /// ```ignore 41 | /// TakeArg::take(&mut <#ty as DeriveArg>::derive(&paths_arg[#idx])) 42 | /// ``` 43 | /// 44 | /// (`#ty` is the type of the function argument). 45 | /// 46 | /// [`DeriveArg`] is responsible for converting `&PathBuf` into an "owned" form of `#ty`. For example, 47 | /// owned form for both `&str` and `String` will be `String` and conversion will be reading the file 48 | /// at given path. 49 | /// 50 | /// [`TakeArg`] is responsible for deriving argument type from the mutable reference to the 51 | /// `TakeArg::Derived`. The reason mutable referenc is used is because, again, the generated code 52 | /// is the same, so we cannot take `self` in one case and `&self` in other case. Instead, we take 53 | /// `&mut self` in `TakeArg` and either convert it to the shared reference or replace the value 54 | /// with "sentinel" (empty value) and return taken out value as result. 55 | /// 56 | /// We pre-define few conversions: 57 | /// 58 | /// 1. `&Path` -> `&str`, `String` (reads file content into a string) 59 | /// 2. `&Path` -> `&[u8]`, `Vec` (reads file content into a byte buffer) 60 | /// 3. `&Path` -> `&Path` (gives path "as is") 61 | /// 62 | /// Conversion is two step: first, we need to derive some value. Second, we need to either borrow 63 | /// from that value (if we need `&str`, for example) or take from that value (if we need `String`, 64 | /// for example) to pass an argument to the function. 65 | #[doc(hidden)] 66 | pub trait DeriveArg<'a>: 'a + Sized { 67 | /// Type to hold temporary value when going from `&Path` into target type. 68 | /// Necessary for conversions from `&Path` to `&str`, 69 | type Derived: TakeArg<'a, Self>; 70 | fn derive(path: &'a Path) -> Self::Derived; 71 | } 72 | 73 | // Strings 74 | 75 | impl<'a> DeriveArg<'a> for &'a str { 76 | type Derived = String; 77 | fn derive(path: &'a Path) -> String { 78 | crate::read_to_string(path) 79 | } 80 | } 81 | 82 | impl<'a> DeriveArg<'a> for String { 83 | type Derived = String; 84 | fn derive(path: &'a Path) -> String { 85 | crate::read_to_string(path) 86 | } 87 | } 88 | 89 | // Byte slices 90 | 91 | impl<'a> DeriveArg<'a> for &'a [u8] { 92 | type Derived = Vec; 93 | fn derive(path: &'a Path) -> Vec { 94 | crate::read_to_end(path) 95 | } 96 | } 97 | 98 | impl<'a> DeriveArg<'a> for Vec { 99 | type Derived = Vec; 100 | fn derive(path: &'a Path) -> Vec { 101 | crate::read_to_end(path) 102 | } 103 | } 104 | 105 | // Paths 106 | 107 | impl<'a> DeriveArg<'a> for &'a Path { 108 | type Derived = &'a Path; 109 | 110 | fn derive(path: &'a Path) -> &'a Path { 111 | path 112 | } 113 | } 114 | 115 | #[doc(hidden)] 116 | pub trait TakeArg<'a, T: 'a> { 117 | fn take(&'a mut self) -> T; 118 | } 119 | 120 | // If we can borrow, we are good! 121 | 122 | impl<'a, T, Q> TakeArg<'a, &'a Q> for T 123 | where 124 | Q: ?Sized, 125 | T: Borrow, 126 | { 127 | fn take(&'a mut self) -> &'a Q { 128 | T::borrow(self) 129 | } 130 | } 131 | 132 | // Otherwise, take the value & leave the empty one. This is guaranteed (by our proc macro) to 133 | // be only called once. 134 | 135 | impl<'a> TakeArg<'a, String> for String { 136 | fn take(&mut self) -> String { 137 | std::mem::take(self) 138 | } 139 | } 140 | 141 | impl<'a> TakeArg<'a, Vec> for Vec { 142 | fn take(&mut self) -> Vec { 143 | std::mem::take(self) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Datatest: data-driven tests in Rust 2 | 3 | [![crates.io][Crate Logo]][Crate] 4 | [![Documentation][Doc Logo]][Doc] 5 | [![Build Status][CI Logo]][CI] 6 | 7 | Crate for supporting data-driven tests. 8 | 9 | Data-driven tests are tests where individual cases are defined via data rather than in code. 10 | This crate implements a custom test runner that adds support for additional test types. 11 | 12 | ## Files-driven test 13 | 14 | First type of data-driven tests are "file-driven" tests. These tests define a directory to 15 | scan for test data, a pattern (a regular expression) to match and, optionally, a set of 16 | templates to derive other file paths based on the matched file name. For each matched file, 17 | a new test instance is created, with test function arguments derived based on the specified 18 | mappings. 19 | 20 | Each argument of the test function must be mapped either to the pattern or to the template. 21 | See the example below for the syntax. 22 | 23 | The following argument types are supported: 24 | * `&str`, `String`: capture file contents as string and pass it to the test function 25 | * `&[u8]`, `Vec`: capture file contents and pass it to the test function 26 | * `&Path`: pass file path as-is 27 | 28 | #### Note 29 | 30 | Each test could also be marked with `#[test]` attribute, to allow running test from IDEs which 31 | have built-in support for `#[test]` tests. However, if such attribute is used, it should go 32 | after `#[datatest::files]` attribute, so `datatest` attribute is handled earlier and `#[test]` 33 | attribute is removed. 34 | 35 | ### Example 36 | 37 | ```rust 38 | #![feature(custom_test_frameworks)] 39 | #![test_runner(datatest::runner)] 40 | 41 | #[datatest::files("tests/test-cases", { 42 | input in r"^(.*).input\.txt", 43 | output = r"${1}.output.txt", 44 | })] 45 | fn sample_test(input: &str, output: &str) { 46 | assert_eq!(format!("Hello, {}!", input), output); 47 | } 48 | ``` 49 | 50 | #### Ignoring individual tests 51 | 52 | Individual tests could be ignored by specifying a function of signature 53 | `fn(&std::path::Path) -> bool` using the following syntax on the pattern (`if !`): 54 | 55 | ```rust 56 | #![feature(custom_test_frameworks)] 57 | #![test_runner(datatest::runner)] 58 | 59 | fn is_ignore(path: &std::path::Path) -> bool { 60 | true // some condition 61 | } 62 | 63 | #[datatest::files("tests/test-cases", { 64 | input in r"^(.*).input\.txt" if !is_ignore, 65 | output = r"${1}.output.txt", 66 | })] 67 | fn sample_test(input: &str, output: &str) { 68 | assert_eq!(format!("Hello, {}!", input), output); 69 | } 70 | ``` 71 | 72 | ## Data-driven tests 73 | 74 | Second type of tests supported by this crate are "data-driven" tests. These tests define a 75 | YAML file with a list of test cases (via `#[datatest::data(..)]` attribute, see example below). 76 | Each test case in this file (the file contents must be an array) is deserialized into the 77 | argument type of the test function and a separate test instance is created for it. 78 | 79 | Test function must take exactly one argument and the type of this argument must implement 80 | [`serde::Deserialize`]. Optionally, if this implements [`ToString`] (or [`std::fmt::Display`]), 81 | it's [`ToString::to_string`] result is used to generate test name. 82 | 83 | #### `#[test]` attribute 84 | 85 | Each test could also be marked with `#[test]` attribute, to allow running test from IDEs which 86 | have built-in support for `#[test]` tests. However, if such attribute is used, it should go 87 | after `#[datatest::files]` attribute, so `datatest` attribute is handled earlier and `#[test]` 88 | attribute is removed. 89 | 90 | ### Example 91 | 92 | ```rust 93 | #![feature(custom_test_frameworks)] 94 | #![test_runner(datatest::runner)] 95 | 96 | use serde::Deserialize; 97 | 98 | #[derive(Deserialize)] 99 | struct TestCase { 100 | name: String, 101 | expected: String, 102 | } 103 | 104 | #[datatest::data("tests/tests.yaml")] 105 | fn sample_test(case: TestCase) { 106 | assert_eq!(case.expected, format!("Hi, {}!", case.name)); 107 | } 108 | 109 | ``` 110 | 111 | ### More examples 112 | 113 | For more examples, check the [tests](https://github.com/commure/datatest/blob/master/tests/datatest.rs). 114 | 115 | # Notes on Rust channel 116 | 117 | Currently this crate targets primarily nightly Rust and can break at any time. 118 | 119 | It could be compiled on stable by enabling certain feature (see `Cargo.toml`), but using this feature would subvert 120 | any stability guarantees Rust provides. 121 | 122 | ## License 123 | 124 | Licensed under either of 125 | 126 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 127 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 128 | 129 | at your option. 130 | 131 | ### Contribution 132 | 133 | Unless you explicitly state otherwise, any contribution intentionally submitted 134 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any 135 | additional terms or conditions. 136 | 137 | 138 | [Crate]: https://crates.io/crates/datatest 139 | [Crate Logo]: https://img.shields.io/crates/v/datatest.svg 140 | 141 | [Doc]: https://docs.rs/datatest 142 | [Doc Logo]: https://docs.rs/datatest/badge.svg 143 | 144 | [CI]: https://dev.azure.com/commure/datatest/_build/latest?definitionId=3&branchName=master 145 | [CI Logo]: https://dev.azure.com/commure/datatest/_apis/build/status/commure.datatest?branchName=master 146 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | #![allow(incomplete_features)] 3 | #![feature(specialization)] 4 | #![feature(unboxed_closures)] 5 | #![feature(fn_traits)] 6 | //! Crate for supporting data-driven tests. 7 | //! 8 | //! Data-driven tests are tests where individual cases are defined via data rather than in code. 9 | //! This crate implements a custom test runner that adds support for additional test types. 10 | //! 11 | //! # Files-driven test 12 | //! 13 | //! First type of data-driven tests are "file-driven" tests. These tests define a directory to 14 | //! scan for test data, a pattern (a regular expression) to match and, optionally, a set of 15 | //! templates to derive other file paths based on the matched file name. For each matched file, 16 | //! a new test instance is created, with test function arguments derived based on the specified 17 | //! mappings. 18 | //! 19 | //! Each argument of the test function must be mapped either to the pattern or to the template. 20 | //! See the example below for the syntax. 21 | //! 22 | //! The following argument types are supported: 23 | //! * `&str`, `String`: capture file contents as string and pass it to the test function 24 | //! * `&[u8]`, `Vec`: capture file contents and pass it to the test function 25 | //! * `&Path`: pass file path as-is 26 | //! 27 | //! ### Note 28 | //! 29 | //! Each test could also be marked with `#[test]` attribute, to allow running test from IDEs which 30 | //! have built-in support for `#[test]` tests. However, if such attribute is used, it should go 31 | //! after `#[datatest::files]` attribute, so `datatest` attribute is handled earlier and `#[test]` 32 | //! attribute is removed. 33 | //! 34 | //! ## Example 35 | //! 36 | //! ```rust 37 | //! #![feature(custom_test_frameworks)] 38 | //! #![test_runner(datatest::runner)] 39 | //! 40 | //! #[datatest::files("tests/test-cases", { 41 | //! input in r"^(.*).input\.txt", 42 | //! output = r"${1}.output.txt", 43 | //! })] 44 | //! fn sample_test(input: &str, output: &str) { 45 | //! assert_eq!(format!("Hello, {}!", input), output); 46 | //! } 47 | //! ``` 48 | //! 49 | //! ### Ignoring individual tests 50 | //! 51 | //! Individual tests could be ignored by specifying a function of signature 52 | //! `fn(&std::path::Path) -> bool` using the following syntax on the pattern (`if !`): 53 | //! 54 | //! ```rust 55 | //! #![feature(custom_test_frameworks)] 56 | //! #![test_runner(datatest::runner)] 57 | //! 58 | //! fn is_ignore(path: &std::path::Path) -> bool { 59 | //! true // some condition 60 | //! } 61 | //! 62 | //! #[datatest::files("tests/test-cases", { 63 | //! input in r"^(.*).input\.txt" if !is_ignore, 64 | //! output = r"${1}.output.txt", 65 | //! })] 66 | //! fn sample_test(input: &str, output: &str) { 67 | //! assert_eq!(format!("Hello, {}!", input), output); 68 | //! } 69 | //! ``` 70 | //! 71 | //! # Data-driven tests 72 | //! 73 | //! Second type of tests supported by this crate are "data-driven" tests. These tests define a 74 | //! YAML file with a list of test cases (via `#[datatest::data(..)]` attribute, see example below). 75 | //! Each test case in this file (the file contents must be an array) is deserialized into the 76 | //! argument type of the test function and a separate test instance is created for it. 77 | //! 78 | //! Test function must take exactly one argument and the type of this argument must implement 79 | //! [`serde::Deserialize`]. Optionally, if this implements [`ToString`] (or [`std::fmt::Display`]), 80 | //! it's [`ToString::to_string`] result is used to generate test name. 81 | //! 82 | //! ### `#[test]` attribute 83 | //! 84 | //! Each test could also be marked with `#[test]` attribute, to allow running test from IDEs which 85 | //! have built-in support for `#[test]` tests. However, if such attribute is used, it should go 86 | //! after `#[datatest::files]` attribute, so `datatest` attribute is handled earlier and `#[test]` 87 | //! attribute is removed. 88 | //! 89 | //! ## Example 90 | //! 91 | //! ```rust 92 | //! #![feature(custom_test_frameworks)] 93 | //! #![test_runner(datatest::runner)] 94 | //! 95 | //! use serde::Deserialize; 96 | //! 97 | //! #[derive(Deserialize)] 98 | //! struct TestCase { 99 | //! name: String, 100 | //! expected: String, 101 | //! } 102 | //! 103 | //! #[datatest::data("tests/tests.yaml")] 104 | //! fn sample_test(case: TestCase) { 105 | //! assert_eq!(case.expected, format!("Hi, {}!", case.name)); 106 | //! } 107 | //! 108 | //! # fn main() {} 109 | //! ``` 110 | //! 111 | //! ## More examples 112 | //! 113 | //! For more examples, check the [tests](https://github.com/commure/datatest/blob/master/tests/datatest.rs). 114 | extern crate test as rustc_test; 115 | 116 | mod data; 117 | mod files; 118 | mod runner; 119 | 120 | #[cfg(feature = "unsafe_test_runner")] 121 | mod interceptor; 122 | 123 | #[cfg(not(feature = "unsafe_test_runner"))] 124 | mod interceptor { 125 | pub fn install_interceptor() {} 126 | } 127 | 128 | /// Internal re-exports for the procedural macro to use. 129 | #[doc(hidden)] 130 | pub mod __internal { 131 | pub use crate::data::{DataBenchFn, DataTestDesc, DataTestFn}; 132 | pub use crate::files::{DeriveArg, FilesTestDesc, FilesTestFn, TakeArg}; 133 | pub use crate::runner::assert_test_result; 134 | pub use crate::rustc_test::Bencher; 135 | pub use ctor::{ctor, dtor}; 136 | 137 | // To maintain registry on stable channel 138 | pub use crate::runner::{ 139 | check_test_runner, register, RegistrationNode, RegularShouldPanic, RegularTestDesc, 140 | }; 141 | // i.e. no TCR, use ctor instead 142 | #[cfg(not(all(feature = "rustc_is_nightly", feature = "test_case_registration")))] 143 | pub use datatest_derive::{data_ctor_internal, files_ctor_internal}; 144 | // i.e. use TCR 145 | #[cfg(all(feature = "rustc_is_nightly", feature = "test_case_registration"))] 146 | pub use datatest_derive::{data_test_case_internal, files_test_case_internal}; 147 | } 148 | 149 | pub use crate::runner::runner; 150 | 151 | // i.e. no TCR, use ctor instead 152 | #[cfg(not(all(feature = "rustc_is_nightly", feature = "test_case_registration")))] 153 | pub use datatest_derive::{ 154 | data_ctor_registration as data, files_ctor_registration as files, 155 | test_ctor_registration as test, 156 | }; 157 | 158 | // i.e. use TCR 159 | #[cfg(all(feature = "rustc_is_nightly", feature = "test_case_registration"))] 160 | pub use datatest_derive::{ 161 | data_test_case_registration as data, files_test_case_registration as files, 162 | }; 163 | 164 | /// Experimental functionality. 165 | #[doc(hidden)] 166 | pub use crate::data::{yaml, DataTestCaseDesc}; 167 | 168 | use std::fs::File; 169 | use std::io::{BufReader, Read}; 170 | use std::path::Path; 171 | 172 | /// `datatest` test harness entry point. Should be declared in the test module, like in the 173 | /// following snippet: 174 | /// ```rust,no_run 175 | /// datatest::harness!(); 176 | /// ``` 177 | /// 178 | /// Also, `harness` should be set to `false` for that test module in `Cargo.toml` (see [Configuring a target](https://doc.rust-lang.org/cargo/reference/manifest.html#configuring-a-target)). 179 | #[macro_export] 180 | macro_rules! harness { 181 | () => { 182 | #[cfg(test)] 183 | fn main() { 184 | ::datatest::runner(&[]); 185 | } 186 | }; 187 | } 188 | 189 | /// Helper function used internally. 190 | fn read_to_string(path: &Path) -> String { 191 | let mut input = String::new(); 192 | File::open(path) 193 | .map(BufReader::new) 194 | .and_then(|mut f| f.read_to_string(&mut input)) 195 | .unwrap_or_else(|e| panic!("cannot read test input at '{}': {}", path.display(), e)); 196 | input 197 | } 198 | 199 | /// Helper function used internally. 200 | fn read_to_end(path: &Path) -> Vec { 201 | let mut input = Vec::new(); 202 | File::open(path) 203 | .map(BufReader::new) 204 | .and_then(|mut f| f.read_to_end(&mut input)) 205 | .unwrap_or_else(|e| panic!("cannot read test input at '{}': {}", path.display(), e)); 206 | input 207 | } 208 | 209 | use crate::rustc_test::TestType; 210 | 211 | /// Helper function used internally, to mirror how rustc_test chooses a TestType. 212 | /// Must be called with the result of `file!()` (called in macro output) to be meaningful. 213 | pub fn test_type(path: &'static str) -> TestType { 214 | if path.starts_with("src") { 215 | // `/src` folder contains unit-tests. 216 | TestType::UnitTest 217 | } else if path.starts_with("tests") { 218 | // `/tests` folder contains integration tests. 219 | TestType::IntegrationTest 220 | } else { 221 | // Crate layout doesn't match expected one, test type is unknown. 222 | TestType::Unknown 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /tests/tests/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::fmt; 3 | use std::path::Path; 4 | 5 | /// File-driven tests are defined via `#[files(...)]` attribute. 6 | /// 7 | /// The first argument to the attribute is the path to the test data (relative to the crate root 8 | /// directory). 9 | /// 10 | /// The second argument is a block of mappings, each mapping defines the rules of deriving test 11 | /// function arguments. 12 | /// 13 | /// Exactly one mapping should be a "pattern" mapping, defined as ` in ""`. `` 14 | /// is a regular expression applied to every file found in the test directory. For each file path 15 | /// matching the regular expression, test runner will create a new test instance. 16 | /// 17 | /// Other mappings are "template" mappings, they define the template to use for deriving the file 18 | /// paths. Each template have a syntax of a [replacement string] from [`regex`] crate. 19 | /// 20 | /// [replacement string]: https://docs.rs/regex/*/regex/struct.Regex.html#method.replace 21 | /// [regex]: https://docs.rs/regex/*/regex/ 22 | #[datatest::files("tests/test-cases", { 23 | // Pattern is defined via `in` operator. Every file from the `directory` above will be matched 24 | // against this regular expression and every matched file will produce a separate test. 25 | input in r"^(.*)\.input\.txt", 26 | // Template defines a rule for deriving dependent file name based on captures of the pattern. 27 | output = r"${1}.output.txt", 28 | })] 29 | #[test] 30 | fn files_test_strings(input: &str, output: &str) { 31 | assert_eq!(format!("Hello, {}!", input), output); 32 | } 33 | 34 | /// Same as above, but always panics, so marked by `#[ignore]` 35 | #[ignore] 36 | #[datatest::files("tests/test-cases", { 37 | input in r"^(.*)\.input\.txt", 38 | output = r"${1}.output.txt", 39 | })] 40 | #[test] 41 | fn files_tests_not_working_yet_and_never_will(input: &str, output: &str) { 42 | assert_eq!(input, output, "these two will never match!"); 43 | } 44 | 45 | /// Always returns an `Err` failure, so marked by `#[ignore]` 46 | #[ignore] 47 | #[datatest::files("tests/test-cases", { 48 | _input in r"^(.*)\.input\.txt", 49 | _output = r"${1}.output.txt", 50 | })] 51 | #[test] 52 | fn tests_may_return_result_err(_input: &str, _output: &str) -> Result<(), String> { 53 | Err("This is a helpful error message that you will now see when you run this test.".to_string()) 54 | } 55 | 56 | /// Same as the first test, but returns `Ok(())` rather than `()`. 57 | #[ignore] 58 | #[datatest::files("tests/test-cases", { 59 | input in r"^(.*)\.input\.txt", 60 | output = r"${1}.output.txt", 61 | })] 62 | #[test] 63 | fn tests_may_return_result_ok(input: &str, output: &str) -> Result<(), String> { 64 | assert_eq!(format!("Hello, {}!", input), output); 65 | Ok(()) 66 | } 67 | 68 | /// Same as above, but uses symbolic files, which is only tested on unix platforms 69 | #[datatest::files("tests/test-cases", { 70 | input in r"^(.*)\.input-linked\.txt", 71 | output = r"${1}.output-linked.txt", 72 | })] 73 | #[test] 74 | #[cfg(unix)] 75 | fn symbolic_files_test_strings(input: &str, output: &str) { 76 | assert_eq!(format!("Hello, {}!", input), output); 77 | } 78 | 79 | /// Can declare with `&std::path::Path` to get path instead of the content 80 | #[datatest::files("tests/test-cases", { 81 | input in r"^(.*)\.input\.txt", 82 | output = r"${1}.output.txt", 83 | })] 84 | #[test] 85 | fn files_test_paths(input: &Path, output: &Path) { 86 | let input = input.display().to_string(); 87 | let output = output.display().to_string(); 88 | // Check output path is indeed input path with `input` => `output` 89 | assert_eq!(input.replace("input", "output"), output); 90 | } 91 | 92 | /// Can also take slices 93 | #[datatest::files("tests/test-cases", { 94 | input in r"^(.*)\.input\.txt", 95 | output = r"${1}.output.txt", 96 | })] 97 | #[test] 98 | fn files_test_slices(input: &[u8], output: &[u8]) { 99 | let mut actual = b"Hello, ".to_vec(); 100 | actual.extend(input); 101 | actual.push(b'!'); 102 | assert_eq!(actual, output); 103 | } 104 | 105 | fn is_ignore(path: &Path) -> bool { 106 | path.display().to_string().ends_with("case-02.input.txt") 107 | } 108 | 109 | /// Ignore first test case! 110 | #[datatest::files("tests/test-cases", { 111 | input in r"^(.*)\.input\.txt" if !is_ignore, 112 | output = r"${1}.output.txt", 113 | })] 114 | #[test] 115 | fn files_test_ignore(input: &str) { 116 | assert_eq!(input, "Kylie"); 117 | } 118 | 119 | /// Regular tests are also allowed! 120 | #[test] 121 | fn simple_test() { 122 | let palindrome = "never odd or even".replace(' ', ""); 123 | let reversed = palindrome.chars().rev().collect::(); 124 | 125 | assert_eq!(palindrome, reversed) 126 | } 127 | 128 | /// Regular tests are also allowed! Also, could be ignored the same! 129 | #[test] 130 | #[ignore] 131 | fn simple_test_ignored() { 132 | panic!("ignored test!") 133 | } 134 | 135 | /// Regular tests are also allowed! Also, could use `#[should_panic]` 136 | #[test] 137 | #[should_panic] 138 | fn simple_test_panics() { 139 | panic!("panicking test!") 140 | } 141 | 142 | /// Regular tests are also allowed! Also, could use `#[should_panic]` 143 | #[test] 144 | #[should_panic(expected = "panicking test!")] 145 | fn simple_test_panics_message() { 146 | panic!("panicking test!") 147 | } 148 | 149 | /// This test case item does not implement [`std::fmt::Display`], so only line number is shown in 150 | /// the test name. 151 | #[derive(Deserialize)] 152 | struct GreeterTestCase { 153 | name: String, 154 | expected: String, 155 | } 156 | 157 | /// Data-driven tests are defined via `#[datatest::data(..)]` attribute. 158 | /// 159 | /// This attribute specifies a test file with test cases. Currently, the test file have to be in 160 | /// YAML format. This file is deserialized into `Vec`, where `T` is the type of the test function 161 | /// argument (which must implement `serde::Deserialize`). Then, for each element of the vector, a 162 | /// separate test instance is created and executed. 163 | /// 164 | /// Name of each test is derived from the test function module path, test case line number and, 165 | /// optionall, from the [`ToString`] implementation of the test case data (if either [`ToString`] 166 | /// or [`std::fmt::Display`] is implemented). 167 | #[datatest::data("tests/tests.yaml")] 168 | #[test] 169 | fn data_test_line_only(data: &GreeterTestCase) { 170 | assert_eq!(data.expected, format!("Hi, {}!", data.name)); 171 | } 172 | 173 | /// Can take as value, too 174 | #[datatest::data("tests/tests.yaml")] 175 | #[test] 176 | fn data_test_take_owned(mut data: GreeterTestCase) { 177 | data.expected += "boo!"; 178 | data.name += "!boo"; 179 | assert_eq!(data.expected, format!("Hi, {}!", data.name)); 180 | } 181 | 182 | #[ignore] 183 | #[datatest::data("tests/tests.yaml")] 184 | #[test] 185 | fn data_test_line_only_hoplessly_broken(_data: &GreeterTestCase) { 186 | panic!("this test always fails, but this is okay because we marked it as ignored!") 187 | } 188 | 189 | /// This test case item implements [`std::fmt::Display`], which is used to generate test name 190 | #[derive(Deserialize)] 191 | struct GreeterTestCaseNamed { 192 | name: String, 193 | expected: String, 194 | } 195 | 196 | impl fmt::Display for GreeterTestCaseNamed { 197 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 198 | f.write_str(&self.name) 199 | } 200 | } 201 | 202 | #[datatest::data("tests/tests.yaml")] 203 | #[test] 204 | fn data_test_name_and_line(data: &GreeterTestCaseNamed) { 205 | assert_eq!(data.expected, format!("Hi, {}!", data.name)); 206 | } 207 | 208 | /// Can also take string inputs 209 | #[datatest::data("tests/strings.yaml")] 210 | #[test] 211 | fn data_test_string(data: String) { 212 | let half = data.len() / 2; 213 | assert_eq!(data[0..half], data[half..]); 214 | } 215 | 216 | /// Can also use `::datatest::yaml` explicitly 217 | #[datatest::data(::datatest::yaml("tests/strings.yaml"))] 218 | #[test] 219 | fn data_test_yaml(data: String) { 220 | let half = data.len() / 2; 221 | assert_eq!(data[0..half], data[half..]); 222 | } 223 | 224 | // Experimental API: allow custom test cases 225 | 226 | struct StringTestCase { 227 | input: String, 228 | output: String, 229 | } 230 | 231 | fn load_test_cases(path: &str) -> Vec<::datatest::DataTestCaseDesc> { 232 | let input = std::fs::read_to_string(path).unwrap(); 233 | let lines = input.lines().collect::>(); 234 | lines 235 | .chunks(2) 236 | .enumerate() 237 | .map(|(idx, line)| ::datatest::DataTestCaseDesc { 238 | case: StringTestCase { 239 | input: line[0].to_string(), 240 | output: line[1].to_string(), 241 | }, 242 | name: Some(line[0].to_string()), 243 | location: format!("line {}", idx * 2), 244 | }) 245 | .collect() 246 | } 247 | 248 | /// Can have custom deserialization for data tests 249 | #[datatest::data(load_test_cases("tests/cases.txt"))] 250 | #[test] 251 | fn data_test_custom(data: StringTestCase) { 252 | assert_eq!(data.output, format!("Hello, {}!", data.input)); 253 | } 254 | -------------------------------------------------------------------------------- /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 2019 Commure, Inc. 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 | -------------------------------------------------------------------------------- /src/runner.rs: -------------------------------------------------------------------------------- 1 | use crate::data::{DataTestDesc, DataTestFn}; 2 | use crate::files::{FilesTestDesc, FilesTestFn}; 3 | use crate::rustc_test::{Bencher, ShouldPanic, TestDesc, TestDescAndFn, TestFn, TestName}; 4 | use std::fmt; 5 | use std::path::{Path, PathBuf}; 6 | use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; 7 | 8 | /// Our own copy of `test::ShouldPanic` to be used on stable channel (using types from `test` crate 9 | /// is not allowed on stable without `#![feature(test)]`. Pretty much copy-pasted. 10 | /// In this crate, though, we "can" use types from `test` due to some extra magic (or, rather, hack) 11 | /// we do in `build.rs`. 12 | #[derive(Clone, Copy)] 13 | pub enum RegularShouldPanic { 14 | No, 15 | Yes, 16 | YesWithMessage(&'static str), 17 | } 18 | 19 | impl From for ShouldPanic { 20 | fn from(value: RegularShouldPanic) -> Self { 21 | match value { 22 | RegularShouldPanic::No => ShouldPanic::No, 23 | RegularShouldPanic::Yes => ShouldPanic::Yes, 24 | RegularShouldPanic::YesWithMessage(msg) => ShouldPanic::YesWithMessage(msg), 25 | } 26 | } 27 | } 28 | 29 | /// Support for regular `#[test]` tests when we run on stable and cannot intercept test descriptors 30 | /// generated by Rust compiler. 31 | /// 32 | #[derive(Clone, Copy)] 33 | pub struct RegularTestDesc { 34 | pub name: &'static str, 35 | pub ignore: bool, 36 | pub testfn: fn(), 37 | pub should_panic: RegularShouldPanic, 38 | pub source_file: &'static str, 39 | } 40 | 41 | impl RegularTestDesc { 42 | pub fn testfunction(&self) -> Result<(), String> { 43 | (self.testfn)(); 44 | Ok(()) 45 | } 46 | } 47 | 48 | fn derive_test_name(root: &Path, path: &Path, test_name: &str) -> String { 49 | let relative = path.strip_prefix(root).unwrap_or_else(|_| { 50 | panic!( 51 | "failed to strip prefix '{}' from path '{}'", 52 | root.display(), 53 | path.display() 54 | ) 55 | }); 56 | let mut test_name = real_name(test_name).to_string(); 57 | test_name += "::"; 58 | test_name += &relative.to_string_lossy(); 59 | test_name 60 | } 61 | 62 | /// When compiling tests, Rust compiler collects all items marked with `#[test_case]` and passes 63 | /// references to them to the test runner in a slice (like `&[&test_a, &test_b, &test_c]`). Since 64 | /// we need a different descriptor for our data-driven tests than the standard one, we have two 65 | /// options here: 66 | /// 67 | /// 1. override standard `#[test]` handling and generate our own descriptor for regular tests, so 68 | /// our runner can accept the descriptor of our own type. 69 | /// 2. accept a trait object in a runner and make both standard descriptor and our custom descriptors 70 | /// to implement that trait and use dynamic dispatch to dispatch on the descriptor type. 71 | /// 72 | /// We go with the second approach as it allows us to keep standard `#[test]` processing. 73 | #[doc(hidden)] 74 | pub trait TestDescriptor { 75 | fn as_datatest_desc(&self) -> DatatestTestDesc; 76 | } 77 | 78 | impl TestDescriptor for TestDescAndFn { 79 | fn as_datatest_desc(&self) -> DatatestTestDesc { 80 | DatatestTestDesc::Test(self) 81 | } 82 | } 83 | 84 | impl TestDescriptor for FilesTestDesc { 85 | fn as_datatest_desc(&self) -> DatatestTestDesc { 86 | DatatestTestDesc::FilesTest(self) 87 | } 88 | } 89 | 90 | impl TestDescriptor for DataTestDesc { 91 | fn as_datatest_desc(&self) -> DatatestTestDesc { 92 | DatatestTestDesc::DataTest(self) 93 | } 94 | } 95 | 96 | impl TestDescriptor for RegularTestDesc { 97 | fn as_datatest_desc(&self) -> DatatestTestDesc { 98 | DatatestTestDesc::RegularTest(self) 99 | } 100 | } 101 | 102 | #[doc(hidden)] 103 | pub enum DatatestTestDesc<'a> { 104 | Test(&'a TestDescAndFn), 105 | FilesTest(&'a FilesTestDesc), 106 | DataTest(&'a DataTestDesc), 107 | RegularTest(&'a RegularTestDesc), 108 | } 109 | 110 | /// Helper function to iterate through all the files in the given directory, skipping hidden files, 111 | /// and return an iterator of their paths. 112 | fn iterate_directory(path: &Path) -> impl Iterator { 113 | walkdir::WalkDir::new(path) 114 | .follow_links(true) 115 | .into_iter() 116 | .map(Result::unwrap) 117 | .filter(|entry| { 118 | entry.file_type().is_file() 119 | && entry 120 | .file_name() 121 | .to_str() 122 | .map_or(false, |s| !s.starts_with('.')) // Skip hidden files 123 | }) 124 | .map(|entry| entry.path().to_path_buf()) 125 | } 126 | 127 | struct FilesBenchFn(fn(&mut Bencher, &[PathBuf]), Vec); 128 | 129 | #[cfg(feature = "rustc_test_TDynBenchFn")] 130 | impl rustc_test::TDynBenchFn for FilesBenchFn { 131 | fn run(&self, harness: &mut Bencher) { 132 | (self.0)(harness, &self.1) 133 | } 134 | } 135 | 136 | impl<'r> Fn<(&'r mut Bencher,)> for FilesBenchFn { 137 | extern "rust-call" fn call(&self, (bencher,): (&'r mut Bencher,)) -> Self::Output { 138 | (self.0)(bencher, &self.1[..]); 139 | Ok(()) 140 | } 141 | } 142 | 143 | impl<'r> FnOnce<(&'r mut Bencher,)> for FilesBenchFn { 144 | type Output = Result<(), String>; 145 | extern "rust-call" fn call_once(self, harness: (&'r mut Bencher,)) -> Self::Output { 146 | (self.0)(harness.0, &self.1); 147 | Ok(()) 148 | } 149 | } 150 | 151 | impl<'r> FnMut<(&'r mut Bencher,)> for FilesBenchFn { 152 | extern "rust-call" fn call_mut(&mut self, harness: (&'r mut Bencher,)) -> Self::Output { 153 | (self.0)(harness.0, &self.1); 154 | Ok(()) 155 | } 156 | } 157 | 158 | /// Generate standard test descriptors ([`test::TestDescAndFn`]) from the descriptor of 159 | /// `#[datatest::files(..)]`. 160 | /// 161 | /// Scans all files in a given directory, finds matching ones and generates a test descriptor for 162 | /// each of them. 163 | fn render_files_test(desc: &FilesTestDesc, rendered: &mut Vec) { 164 | let root = Path::new(desc.root).to_path_buf(); 165 | 166 | let pattern = desc.params[desc.pattern]; 167 | let re = regex::Regex::new(pattern) 168 | .unwrap_or_else(|_| panic!("invalid regular expression: '{}'", pattern)); 169 | 170 | let mut found = false; 171 | for path in iterate_directory(&root) { 172 | let input_path = path.to_string_lossy(); 173 | if re.is_match(&input_path) { 174 | // Generate list of paths to pass to the test function. We generate a `PathBuf` for each 175 | // argument of the test function and pass them to the trampoline function in a slice. 176 | // See `datatest-derive` proc macro sources for more details. 177 | let mut paths = Vec::with_capacity(desc.params.len()); 178 | 179 | let path_str = path.to_string_lossy(); 180 | for (idx, param) in desc.params.iter().enumerate() { 181 | if idx == desc.pattern { 182 | // Pattern path 183 | paths.push(path.to_path_buf()); 184 | } else { 185 | let rendered_path = re.replace_all(&path_str, *param); 186 | let rendered_path = Path::new(rendered_path.as_ref()).to_path_buf(); 187 | paths.push(rendered_path); 188 | } 189 | } 190 | 191 | let test_name = derive_test_name(&root, &path, desc.name); 192 | let ignore = desc.ignore 193 | || desc 194 | .ignorefn 195 | .map_or(false, |ignore_func| ignore_func(&path)); 196 | 197 | let testfn = match desc.testfn { 198 | FilesTestFn::TestFn(testfn) => TestFn::DynTestFn(Box::new(move || { 199 | testfn(&paths); 200 | Ok(()) 201 | })), 202 | FilesTestFn::BenchFn(benchfn) => { 203 | TestFn::DynBenchFn(Box::new(FilesBenchFn(benchfn, paths))) 204 | } 205 | }; 206 | 207 | // Generate a standard test descriptor 208 | let desc = TestDescAndFn { 209 | desc: TestDesc { 210 | name: TestName::DynTestName(test_name), 211 | ignore, 212 | should_panic: ShouldPanic::No, 213 | // Cannot be used on stable: https://github.com/rust-lang/rust/issues/46488 214 | #[cfg(feature = "rustc_test_Allow_fail")] 215 | allow_fail: false, 216 | test_type: crate::test_type(desc.source_file), 217 | no_run: false, 218 | compile_fail: false, 219 | #[cfg(feature = "rustc_test_Ignore_messages")] 220 | ignore_message: None, 221 | source_file: desc.source_file, 222 | // TODO: 223 | start_line: 0, 224 | start_col: 0, 225 | end_col: 0, 226 | end_line: 0, 227 | }, 228 | testfn, 229 | }; 230 | 231 | rendered.push(desc); 232 | found = true; 233 | } 234 | } 235 | 236 | // We want to avoid silent fails due to typos in regexp! 237 | if !found { 238 | panic!( 239 | "no test cases found for test '{}'. Scanned directory: '{}' with pattern '{}'", 240 | desc.name, desc.root, pattern, 241 | ); 242 | } 243 | } 244 | 245 | fn render_data_test(desc: &DataTestDesc, rendered: &mut Vec) { 246 | let prefix_name = real_name(desc.name); 247 | 248 | let cases = (desc.describefn)(); 249 | for case in cases { 250 | // FIXME: use name provided in `case`... 251 | 252 | let case_name = if let Some(n) = case.name { 253 | format!("{}::{} ({})", prefix_name, n, case.location) 254 | } else { 255 | format!("{}::{}", prefix_name, case.location) 256 | }; 257 | 258 | let testfn = match case.case { 259 | DataTestFn::TestFn(testfn) => TestFn::DynTestFn(Box::new(move || { 260 | testfn(); 261 | Ok(()) 262 | })), 263 | DataTestFn::BenchFn(benchfn) => TestFn::DynBenchFn(Box::new(move |b| { 264 | benchfn(b); 265 | Ok(()) 266 | })), 267 | }; 268 | 269 | // Generate a standard test descriptor 270 | let desc = TestDescAndFn { 271 | desc: TestDesc { 272 | name: TestName::DynTestName(case_name), 273 | ignore: desc.ignore, 274 | should_panic: ShouldPanic::No, 275 | #[cfg(feature = "rustc_test_Allow_fail")] 276 | allow_fail: false, 277 | test_type: crate::test_type(desc.source_file), 278 | compile_fail: false, 279 | no_run: false, 280 | #[cfg(feature = "rustc_test_Ignore_messages")] 281 | ignore_message: None, 282 | source_file: desc.source_file, 283 | // TODO: 284 | start_col: 0, 285 | start_line: 0, 286 | end_col: 0, 287 | end_line: 0, 288 | }, 289 | testfn, 290 | }; 291 | 292 | rendered.push(desc); 293 | } 294 | } 295 | 296 | /// We need to build our own slice of test descriptors to pass to `test::test_main`. We cannot 297 | /// clone `TestFn`, so we do it via matching on variants. Not sure how to handle `Dynamic*` variants, 298 | /// but we seem not to get them here anyway?. 299 | fn clone_testfn(testfn: &TestFn) -> TestFn { 300 | match testfn { 301 | TestFn::StaticTestFn(func) => TestFn::StaticTestFn(*func), 302 | TestFn::StaticBenchFn(bench) => TestFn::StaticBenchFn(*bench), 303 | _ => unimplemented!("only static functions are supported"), 304 | } 305 | } 306 | 307 | /// Strip crate name. We use `module_path!` macro to generate this name, which includes crate name. 308 | /// However, standard test library does not include crate name into a test name. 309 | fn real_name(name: &str) -> &str { 310 | match name.find("::") { 311 | Some(pos) => &name[pos + 2..], 312 | None => name, 313 | } 314 | } 315 | 316 | /// When we have "--exact" option and test filter is exactly our "parent" test (which is nota a real 317 | /// test, but a template for children tests), we adjust options a bit to run all children tests 318 | /// instead. 319 | fn adjust_for_test_name(opts: &mut crate::rustc_test::TestOpts, name: &str) { 320 | let real_test_name = real_name(name); 321 | // rustc 1.52.0 changes `filters` to accept multiple filters from the command line 322 | #[cfg(feature = "rustc_test_TestOpts_filters_vec")] 323 | { 324 | if opts.filter_exact { 325 | if let Some(test_name) = opts.filters.iter_mut().find(|s| *s == real_test_name) { 326 | test_name.push_str("::"); 327 | opts.filter_exact = false; 328 | } 329 | } 330 | } 331 | // fallback for rust < 1.52 332 | #[cfg(not(feature = "rustc_test_TestOpts_filters_vec"))] 333 | { 334 | if opts.filter_exact && opts.filter.as_ref().map_or(false, |s| s == real_test_name) { 335 | if let Some(test_name) = opts.filter.as_mut() { 336 | test_name.push_str("::"); 337 | opts.filter_exact = false; 338 | } 339 | } 340 | } 341 | } 342 | 343 | pub struct RegistrationNode { 344 | pub descriptor: &'static dyn TestDescriptor, 345 | pub next: Option<&'static RegistrationNode>, 346 | } 347 | 348 | static REGISTRY: AtomicPtr = AtomicPtr::new(std::ptr::null_mut()); 349 | 350 | static REGISTRY_USED: AtomicBool = AtomicBool::new(false); 351 | 352 | pub fn register(new: &mut RegistrationNode) { 353 | // Install interceptor that will catch invocation of `test_main_static` so we can collect all 354 | // the test cases annotated with `#[test]` (built-in tests). This is needed to support regular 355 | // `#[test]` tests on stable channel where we don't have a way to override test runner. 356 | crate::interceptor::install_interceptor(); 357 | 358 | // REGISTRY is a linked list that all the registration functions attempt to push to. This is 359 | // the push function. 360 | // 361 | // Since the registration functions are triggered by `rust-ctor` at executable startup, all the 362 | // registration functions will run sequentially. Further, there will be no overlap between this 363 | // list push and the list pops that execute during runner(). So it's all good. 364 | let reg = ®ISTRY; 365 | let mut current = reg.load(Ordering::SeqCst); 366 | loop { 367 | let previous = match reg.compare_exchange(current, new, Ordering::SeqCst, Ordering::SeqCst) 368 | { 369 | Ok(x) => x, 370 | Err(x) => x, 371 | }; 372 | 373 | if previous == current { 374 | new.next = unsafe { previous.as_ref() }; 375 | return; 376 | } else { 377 | current = previous; 378 | } 379 | } 380 | } 381 | 382 | /// Custom test runner. Expands test definitions given in the format our test framework understands 383 | /// ([DataTestDesc]) into definitions understood by Rust test framework ([TestDescAndFn] structs). 384 | /// For regular tests, mapping is one-to-one, for our data driven tests, we generate as many 385 | /// descriptors as test cases we discovered. 386 | /// 387 | /// # Notes 388 | /// So, how does it work? We use a nightly-only feature of [custom_test_frameworks] that allows you 389 | /// to annotate arbitrary function, const or static with `#[test_case]`. Attribute. Then, Rust 390 | /// compiler would transform the code to pass all the discovered test cases as one big slice to the 391 | /// test runner. 392 | /// 393 | /// However, we also want to support standard `#[test]` without disrupting them as much as possible. 394 | /// Internally, compiler would also desugar them to the `#[test_case]` attribute, but the type of 395 | /// the descriptor struct would be a predefined type of `test::TestDescAndFn`. This type, however, 396 | /// cannot represent all the additional information we need for our tests. 397 | /// 398 | /// So we do a little trick here: we rely on the fact that compiler generates code exactly like in 399 | /// the following snippet: 400 | /// 401 | /// ```ignore 402 | /// test::test_main_static(&[&__test_reexports::some::test1, &__test_reexports::some::test2]) 403 | /// ``` 404 | /// 405 | /// Then, we implement `TestDescriptor` trait for the standard test descriptor struct, which would 406 | /// generate trait objects for these structs and pass a trait object instead. We do the same for 407 | /// our structs and our trait object allows us to return the reference wrapped into an enum 408 | /// distinguishing between three different test variants (standard tests, "files" tests and "data" 409 | /// tests). 410 | /// 411 | /// [custom_test_frameworks]: https://github.com/rust-lang/rust/blob/master/src/doc/unstable-book/src/language-features/custom-test-frameworks.md 412 | /// See 413 | #[doc(hidden)] 414 | pub fn runner(tests: &[&dyn TestDescriptor]) { 415 | let args = std::env::args().collect::>(); 416 | let parsed = crate::rustc_test::test::parse_opts(&args); 417 | let mut opts = match parsed { 418 | Some(Ok(o)) => o, 419 | Some(Err(msg)) => panic!("{:?}", msg), 420 | None => return, 421 | }; 422 | 423 | let mut rendered: Vec = Vec::new(); 424 | for input in tests.iter() { 425 | render_test_descriptor(*input, &mut opts, &mut rendered); 426 | } 427 | 428 | // Indicate that we used our registry 429 | REGISTRY_USED.store(true, Ordering::SeqCst); 430 | 431 | // Gather tests registered via our registry (stable channel) 432 | let mut current = unsafe { REGISTRY.load(Ordering::SeqCst).as_ref() }; 433 | while let Some(node) = current { 434 | render_test_descriptor(node.descriptor, &mut opts, &mut rendered); 435 | current = node.next; 436 | } 437 | 438 | // Run tests via standard runner! 439 | match crate::rustc_test::run_tests_console(&opts, rendered) { 440 | Ok(true) => {} 441 | Ok(false) => panic!("Some tests failed"), 442 | Err(e) => panic!("io error when running tests: {:?}", e), 443 | } 444 | } 445 | 446 | fn render_test_descriptor( 447 | input: &dyn TestDescriptor, 448 | opts: &mut crate::rustc_test::TestOpts, 449 | rendered: &mut Vec, 450 | ) { 451 | match input.as_datatest_desc() { 452 | DatatestTestDesc::Test(test) => { 453 | // Make a copy as we cannot take ownership 454 | rendered.push(TestDescAndFn { 455 | desc: test.desc.clone(), 456 | testfn: clone_testfn(&test.testfn), 457 | }) 458 | } 459 | DatatestTestDesc::FilesTest(files) => { 460 | render_files_test(files, rendered); 461 | adjust_for_test_name(opts, files.name); 462 | } 463 | DatatestTestDesc::DataTest(data) => { 464 | render_data_test(data, rendered); 465 | adjust_for_test_name(opts, data.name); 466 | } 467 | DatatestTestDesc::RegularTest(desc) => { 468 | let desc = desc.clone(); 469 | rendered.push(TestDescAndFn { 470 | desc: TestDesc { 471 | name: TestName::StaticTestName(real_name(desc.name)), 472 | ignore: desc.ignore, 473 | should_panic: desc.should_panic.into(), 474 | // FIXME: should support! 475 | #[cfg(feature = "rustc_test_Allow_fail")] 476 | allow_fail: false, 477 | test_type: crate::test_type(desc.source_file), 478 | compile_fail: false, 479 | no_run: false, 480 | #[cfg(feature = "rustc_test_Ignore_messages")] 481 | ignore_message: None, 482 | source_file: desc.source_file, 483 | // TODO: 484 | start_col: 0, 485 | start_line: 0, 486 | end_col: 0, 487 | end_line: 0, 488 | }, 489 | testfn: TestFn::DynTestFn(Box::new(move || desc.testfunction())), 490 | }) 491 | } 492 | } 493 | } 494 | 495 | /// Make sure we our registry was actually scanned! 496 | /// This would detect scenario where none of the ways are used to plug datatest 497 | /// test runner (either by replacing the whole harness or by overriding test runner). 498 | /// So, for every test we have registered, we make sure this test actually gets 499 | pub fn check_test_runner() { 500 | if !REGISTRY_USED.load(Ordering::SeqCst) { 501 | panic!("test runner was not configured!"); 502 | } 503 | } 504 | 505 | pub trait Termination { 506 | fn report(self) -> i32; 507 | } 508 | 509 | impl Termination for Result<(), E> { 510 | fn report(self) -> i32 { 511 | match self { 512 | Ok(()) => ().report(), 513 | Err(err) => { 514 | eprintln!("Error: {:?}", err); 515 | // FIXME This should really be system-specific, but std around 516 | // this area looks very unstable at the moment. 517 | 255 518 | } 519 | } 520 | } 521 | } 522 | 523 | impl Termination for () { 524 | #[inline] 525 | fn report(self) -> i32 { 526 | 0 527 | } 528 | } 529 | 530 | #[doc(hidden)] 531 | pub fn assert_test_result(result: T) { 532 | let code = result.report(); 533 | assert_eq!( 534 | code, 0, 535 | "the test returned a termination value with a non-zero status code ({}) \ 536 | which indicates a failure", 537 | code 538 | ); 539 | } 540 | -------------------------------------------------------------------------------- /datatest-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "128"] 2 | #![deny(unused_must_use)] 3 | extern crate proc_macro; 4 | 5 | use proc_macro2::{Span, TokenStream}; 6 | use quote::quote; 7 | use std::collections::HashMap; 8 | use syn::parse::{Parse, ParseStream, Result as ParseResult}; 9 | use syn::punctuated::Punctuated; 10 | use syn::spanned::Spanned; 11 | use syn::token::Comma; 12 | use syn::{braced, parse_macro_input, FnArg, Ident, ItemFn, Pat, PatIdent, PatType, Token, Type}; 13 | 14 | type Error = syn::parse::Error; 15 | 16 | struct TemplateArg { 17 | ident: syn::Ident, 18 | is_pattern: bool, 19 | ignore_fn: Option, 20 | value: syn::LitStr, 21 | } 22 | 23 | impl Parse for TemplateArg { 24 | fn parse(input: ParseStream) -> ParseResult { 25 | let mut ignore_fn = None; 26 | let ident = input.parse::()?; 27 | 28 | let is_pattern = if input.peek(syn::token::In) { 29 | let _in = input.parse::()?; 30 | true 31 | } else { 32 | let _eq = input.parse::()?; 33 | false 34 | }; 35 | let value = input.parse::()?; 36 | if is_pattern && input.peek(syn::token::If) { 37 | let _if = input.parse::()?; 38 | let _not = input.parse::()?; 39 | ignore_fn = Some(input.parse::()?); 40 | } 41 | Ok(Self { 42 | ident, 43 | is_pattern, 44 | ignore_fn, 45 | value, 46 | }) 47 | } 48 | } 49 | 50 | /// Parse `#[file_test(...)]` attribute arguments 51 | /// The syntax is the following: 52 | /// 53 | /// ```ignore 54 | /// #[files("", { 55 | /// in "", 56 | /// in "