diff --git a/Cargo.toml b/Cargo.toml index d506a18..56acd35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "datatest" -version = "0.3.5" +version = "0.4.0" authors = ["Ivan Dubrov "] edition = "2018" repository = "https://github.com/commure/datatest" @@ -10,13 +10,21 @@ description = """ Data-driven tests in Rust """ +[[test]] +name = "datatest_stable" +harness = false + +[build-dependencies] +version_check = "0.9.1" + [dependencies] -datatest-derive = { path = "datatest-derive", version = "=0.3.5" } +datatest-derive = { path = "datatest-derive", version = "=0.4.0" } regex = "1.0.0" walkdir = "2.1.4" serde = "1.0.84" serde_yaml = "0.8.7" yaml-rust = "0.4.2" +ctor = "0.1.10" [dev-dependencies] serde = { version = "1.0.84", features = ["derive"] } diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..91b4f22 --- /dev/null +++ b/build.rs @@ -0,0 +1,11 @@ +use version_check::Channel; + +fn main() { + let is_nightly = Channel::read().map_or(false, |ch| ch.is_nightly()); + if is_nightly { + println!("cargo:rustc-cfg=feature=\"nightly\""); + } else { + println!("cargo:rustc-cfg=feature=\"stable\""); + } + println!("cargo:rustc-env=RUSTC_BOOTSTRAP=1"); +} diff --git a/datatest-derive/Cargo.toml b/datatest-derive/Cargo.toml index 02b0f7a..246aa72 100644 --- a/datatest-derive/Cargo.toml +++ b/datatest-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "datatest-derive" -version = "0.3.5" +version = "0.4.0" authors = ["Ivan Dubrov "] edition = "2018" repository = "https://github.com/commure/datatest" diff --git a/datatest-derive/src/lib.rs b/datatest-derive/src/lib.rs index 53ea106..fcf3915 100644 --- a/datatest-derive/src/lib.rs +++ b/datatest-derive/src/lib.rs @@ -2,19 +2,14 @@ #![deny(unused_must_use)] extern crate proc_macro; -#[macro_use] -extern crate syn; -#[macro_use] -extern crate quote; -extern crate proc_macro2; - use proc_macro2::{Span, TokenStream}; +use quote::quote; use std::collections::HashMap; use syn::parse::{Parse, ParseStream, Result as ParseResult}; use syn::punctuated::Punctuated; use syn::spanned::Spanned; use syn::token::Comma; -use syn::{ArgCaptured, FnArg, Ident, ItemFn, Pat}; +use syn::{braced, parse_macro_input, ArgCaptured, FnArg, Ident, ItemFn, Pat}; type Error = syn::parse::Error; @@ -90,6 +85,29 @@ impl Parse for FilesTestArgs { } } +enum Channel { + Stable, + Nightly, +} + +/// Wrapper that turns on behavior that works on stable Rust. +#[proc_macro_attribute] +pub fn files_stable( + args: proc_macro::TokenStream, + func: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + files_internal(args, func, Channel::Stable) +} + +/// Wrapper that turns on behavior that works only on nightly Rust. +#[proc_macro_attribute] +pub fn files_nightly( + args: proc_macro::TokenStream, + func: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + files_internal(args, func, Channel::Nightly) +} + /// Proc macro handling `#[files(...)]` syntax. This attribute defines rules for deriving /// test function arguments from file paths. There are two types of rules: /// 1. Pattern rule, ` in ""` @@ -131,11 +149,10 @@ impl Parse for FilesTestArgs { /// I could have made this proc macro to handle these cases explicitly and generate a different /// code, but I decided to not add a complexity of type analysis to the proc macro and use traits /// instead. See `datatest::TakeArg` and `datatest::DeriveArg` to see how this mechanism works. -#[proc_macro_attribute] -#[allow(clippy::needless_pass_by_value)] -pub fn files( +fn files_internal( args: proc_macro::TokenStream, func: proc_macro::TokenStream, + channel: Channel, ) -> proc_macro::TokenStream { let mut func_item = parse_macro_input!(func as ItemFn); let args: FilesTestArgs = parse_macro_input!(args as FilesTestArgs); @@ -195,7 +212,7 @@ pub fn files( params.push(arg.value.value()); invoke_args.push(quote! { - ::datatest::TakeArg::take(&mut <#ty as ::datatest::DeriveArg>::derive(&paths_arg[#idx])) + ::datatest::__internal::TakeArg::take(&mut <#ty as ::datatest::__internal::DeriveArg>::derive(&paths_arg[#idx])) }) } else { return Error::new(pat_ident.span(), "mapping is not defined for the argument") @@ -231,31 +248,34 @@ pub fn files( let orig_func_name = &func_item.ident; let (kind, bencher_param) = if info.bench { - (quote!(BenchFn), quote!(bencher: &mut ::datatest::Bencher,)) + ( + quote!(BenchFn), + quote!(bencher: &mut ::datatest::__internal::Bencher,), + ) } else { (quote!(TestFn), quote!()) }; - // Adding `#[allow(unused_attributes)]` to `#orig_func` to allow `#[ignore]` attribute + let registration = test_registration(channel, &desc_ident); let output = quote! { - #[test_case] + #registration #[automatically_derived] #[allow(non_upper_case_globals)] - static #desc_ident: ::datatest::FilesTestDesc = ::datatest::FilesTestDesc { + static #desc_ident: ::datatest::__internal::FilesTestDesc = ::datatest::__internal::FilesTestDesc { name: concat!(module_path!(), "::", #func_name_str), ignore: #ignore, root: #root, params: &[#(#params),*], pattern: #pattern_idx, ignorefn: #ignore_func_ref, - testfn: ::datatest::FilesTestFn::#kind(#trampoline_func_ident), + testfn: ::datatest::__internal::FilesTestFn::#kind(#trampoline_func_ident), }; #[automatically_derived] #[allow(non_snake_case)] fn #trampoline_func_ident(#bencher_param paths_arg: &[::std::path::PathBuf]) { let result = #orig_func_name(#(#invoke_args),*); - datatest::assert_test_result(result); + ::datatest::__internal::assert_test_result(result); } #func_item @@ -323,11 +343,28 @@ impl Parse for DataTestArgs { } } +/// Wrapper that turns on behavior that works on stable Rust. #[proc_macro_attribute] -#[allow(clippy::needless_pass_by_value)] -pub fn data( +pub fn data_stable( args: proc_macro::TokenStream, func: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + data_internal(args, func, Channel::Stable) +} + +/// Wrapper that turns on behavior that works only on nightly Rust. +#[proc_macro_attribute] +pub fn data_nightly( + args: proc_macro::TokenStream, + func: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + data_internal(args, func, Channel::Nightly) +} + +fn data_internal( + args: proc_macro::TokenStream, + func: proc_macro::TokenStream, + channel: Channel, ) -> proc_macro::TokenStream { let mut func_item = parse_macro_input!(func as ItemFn); let cases: DataTestArgs = parse_macro_input!(args as DataTestArgs); @@ -376,23 +413,24 @@ pub fn data( let (case_ctor, bencher_param, bencher_arg) = if info.bench { ( - quote!(::datatest::DataTestFn::BenchFn(Box::new(::datatest::DataBenchFn(#trampoline_func_ident, case)))), - quote!(bencher: &mut ::datatest::Bencher,), + quote!(::datatest::__internal::DataTestFn::BenchFn(Box::new(::datatest::__internal::DataBenchFn(#trampoline_func_ident, case)))), + quote!(bencher: &mut ::datatest::__internal::Bencher,), quote!(bencher,), ) } else { ( - quote!(::datatest::DataTestFn::TestFn(Box::new(move || #trampoline_func_ident(case)))), + quote!(::datatest::__internal::DataTestFn::TestFn(Box::new(move || #trampoline_func_ident(case)))), quote!(), quote!(), ) }; + let registration = test_registration(channel, &desc_ident); let output = quote! { - #[test_case] + #registration #[automatically_derived] #[allow(non_upper_case_globals)] - static #desc_ident: ::datatest::DataTestDesc = ::datatest::DataTestDesc { + static #desc_ident: ::datatest::__internal::DataTestDesc = ::datatest::__internal::DataTestDesc { name: concat!(module_path!(), "::", #func_name_str), ignore: #ignore, describefn: #describe_func_ident, @@ -402,12 +440,12 @@ pub fn data( #[allow(non_snake_case)] fn #trampoline_func_ident(#bencher_param arg: #ty) { let result = #orig_func_ident(#bencher_arg #ref_token arg); - datatest::assert_test_result(result); + ::datatest::__internal::assert_test_result(result); } #[automatically_derived] #[allow(non_snake_case)] - fn #describe_func_ident() -> Vec<::datatest::DataTestCaseDesc<::datatest::DataTestFn>> { + fn #describe_func_ident() -> Vec<::datatest::DataTestCaseDesc<::datatest::__internal::DataTestFn>> { let result = #cases .into_iter() .map(|input| { @@ -427,3 +465,29 @@ pub fn data( }; output.into() } + +fn test_registration(channel: Channel, desc_ident: &syn::Ident) -> TokenStream { + match channel { + // On nightly, we rely on `custom_test_frameworks` feature + Channel::Nightly => quote!(#[test_case]), + // On stable, we use `ctor` crate to build a registry of all our tests + Channel::Stable => { + let registration_fn = + syn::Ident::new(&format!("{}__REGISTRATION", desc_ident), desc_ident.span()); + let tokens = quote! { + #[allow(non_snake_case)] + #[datatest::__internal::ctor] + fn #registration_fn() { + use ::datatest::__internal::RegistrationNode; + static mut REGISTRATION: RegistrationNode = RegistrationNode { + descriptor: &#desc_ident, + next: None, + }; + // This runs only once during initialization, so should be safe + ::datatest::__internal::register(unsafe { &mut REGISTRATION }); + } + }; + tokens + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 32be0ea..a939373 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -115,17 +115,28 @@ mod data; mod files; mod runner; +/// Internal re-exports for the procedural macro to use. #[doc(hidden)] -pub use crate::data::{DataBenchFn, DataTestDesc, DataTestFn}; -#[doc(hidden)] -pub use crate::files::{DeriveArg, FilesTestDesc, FilesTestFn, TakeArg}; -#[doc(hidden)] -pub use crate::runner::{assert_test_result, runner}; +pub mod __internal { + pub use crate::data::{DataBenchFn, DataTestDesc, DataTestFn}; + pub use crate::files::{DeriveArg, FilesTestDesc, FilesTestFn, TakeArg}; + pub use crate::runner::assert_test_result; + pub use crate::test::Bencher; + pub use ctor::ctor; + + // To maintain registry on stable channel + pub use crate::runner::{register, RegistrationNode}; +} + +pub use crate::runner::runner; + #[doc(hidden)] -pub use crate::test::Bencher; +#[cfg(feature = "stable")] +pub use datatest_derive::{data_stable as data, files_stable as files}; #[doc(hidden)] -pub use datatest_derive::{data, files}; +#[cfg(feature = "nightly")] +pub use datatest_derive::{data_nightly as data, files_nightly as files}; /// Experimental functionality. #[doc(hidden)] diff --git a/src/runner.rs b/src/runner.rs index eaccce7..6f198d1 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -2,6 +2,7 @@ use crate::data::{DataTestDesc, DataTestFn}; use crate::files::{FilesTestDesc, FilesTestFn}; use crate::test::{ShouldPanic, TestDesc, TestDescAndFn, TestFn, TestName}; use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicPtr, Ordering}; fn derive_test_name(root: &Path, path: &Path, test_name: &str) -> String { let relative = path.strip_prefix(root).unwrap_or_else(|_| { @@ -167,7 +168,7 @@ fn render_data_test(desc: &DataTestDesc, rendered: &mut Vec) { }; let testfn = match case.case { - DataTestFn::TestFn(testfn) => TestFn::DynTestFn(Box::new(|| testfn())), + DataTestFn::TestFn(testfn) => TestFn::DynTestFn(testfn), DataTestFn::BenchFn(benchfn) => TestFn::DynBenchFn(benchfn), }; @@ -217,6 +218,27 @@ fn adjust_for_test_name(opts: &mut crate::test::TestOpts, name: &str) { } } +pub struct RegistrationNode { + pub descriptor: &'static dyn TestDescriptor, + pub next: Option<&'static RegistrationNode>, +} + +static REGISTRY: AtomicPtr = AtomicPtr::new(std::ptr::null_mut()); + +pub fn register(new: &mut RegistrationNode) { + let reg = ®ISTRY; + let mut current = reg.load(Ordering::SeqCst); + loop { + let previous = reg.compare_and_swap(current, new, Ordering::SeqCst); + if previous == current { + new.next = unsafe { previous.as_ref() }; + return; + } else { + current = previous; + } + } +} + /// Custom test runner. Expands test definitions given in the format our test framework understands /// ([DataTestDesc]) into definitions understood by Rust test framework ([TestDescAndFn] structs). /// For regular tests, mapping is one-to-one, for our data driven tests, we generate as many @@ -259,23 +281,14 @@ pub fn runner(tests: &[&dyn TestDescriptor]) { let mut rendered: Vec = Vec::new(); for input in tests.iter() { - match input.as_datatest_desc() { - DatatestTestDesc::Test(test) => { - // Make a copy as we cannot take ownership - rendered.push(TestDescAndFn { - desc: test.desc.clone(), - testfn: clone_testfn(&test.testfn), - }) - } - DatatestTestDesc::FilesTest(files) => { - render_files_test(files, &mut rendered); - adjust_for_test_name(&mut opts, &files.name); - } - DatatestTestDesc::DataTest(data) => { - render_data_test(data, &mut rendered); - adjust_for_test_name(&mut opts, &data.name); - } - } + render_test_descriptor(*input, &mut opts, &mut rendered); + } + + // Gather tests registered via our registry (stable channel) + let mut current = unsafe { REGISTRY.load(Ordering::SeqCst).as_ref() }; + while let Some(node) = current { + render_test_descriptor(node.descriptor, &mut opts, &mut rendered); + current = node.next; } // Run tests via standard runner! @@ -286,13 +299,51 @@ pub fn runner(tests: &[&dyn TestDescriptor]) { } } +fn render_test_descriptor( + input: &dyn TestDescriptor, + opts: &mut crate::test::TestOpts, + rendered: &mut Vec, +) { + match input.as_datatest_desc() { + DatatestTestDesc::Test(test) => { + // Make a copy as we cannot take ownership + rendered.push(TestDescAndFn { + desc: test.desc.clone(), + testfn: clone_testfn(&test.testfn), + }) + } + DatatestTestDesc::FilesTest(files) => { + render_files_test(files, rendered); + adjust_for_test_name(opts, &files.name); + } + DatatestTestDesc::DataTest(data) => { + render_data_test(data, rendered); + adjust_for_test_name(opts, &data.name); + } + } +} + +pub trait Termination { + fn is_success(&self) -> bool; +} + +impl Termination for () { + fn is_success(&self) -> bool { + true + } +} + +impl Termination for Result { + fn is_success(&self) -> bool { + self.is_ok() + } +} + #[doc(hidden)] -pub fn assert_test_result(result: T) { - let code = result.report(); - assert_eq!( - code, 0, - "the test returned a termination value with a non-zero status code ({}) \ - which indicates a failure", - code +pub fn assert_test_result(result: T) { + assert!( + result.is_success(), + "the test returned a termination value with a non-zero status code (255) \ + which indicates a failure" ); } diff --git a/tests/datatest.rs b/tests/datatest.rs index fd40ff3..92194c9 100644 --- a/tests/datatest.rs +++ b/tests/datatest.rs @@ -1,208 +1,5 @@ +#![cfg(feature = "nightly")] #![feature(custom_test_frameworks)] #![test_runner(datatest::runner)] -use serde::Deserialize; -use std::fmt; -use std::path::Path; - -/// File-driven tests are defined via `#[files(...)]` attribute. -/// -/// The first argument to the attribute is the path to the test data (relative to the crate root -/// directory). -/// -/// The second argument is a block of mappings, each mapping defines the rules of deriving test -/// function arguments. -/// -/// Exactly one mapping should be a "pattern" mapping, defined as ` in ""`. `` -/// is a regular expression applied to every file found in the test directory. For each file path -/// matching the regular expression, test runner will create a new test instance. -/// -/// Other mappings are "template" mappings, they define the template to use for deriving the file -/// paths. Each template have a syntax of a [replacement string] from [`regex`] crate. -/// -/// [replacement string]: https://docs.rs/regex/*/regex/struct.Regex.html#method.replace -/// [regex]: https://docs.rs/regex/*/regex/ -#[datatest::files("tests/test-cases", { - // Pattern is defined via `in` operator. Every file from the `directory` above will be matched - // against this regular expression and every matched file will produce a separate test. - input in r"^(.*)\.input\.txt", - // Template defines a rule for deriving dependent file name based on captures of the pattern. - output = r"${1}.output.txt", -})] -#[test] -fn files_test_strings(input: &str, output: &str) { - assert_eq!(format!("Hello, {}!", input), output); -} - -/// Same as above, but always panics, so marked by `#[ignore]` -#[ignore] -#[datatest::files("tests/test-cases", { - input in r"^(.*)\.input\.txt", - output = r"${1}.output.txt", -})] -#[test] -fn files_tests_not_working_yet_and_never_will(input: &str, output: &str) { - assert_eq!(input, output, "these two will never match!"); -} - -/// Can declare with `&std::path::Path` to get path instead of the content -#[datatest::files("tests/test-cases", { - input in r"^(.*)\.input\.txt", - output = r"${1}.output.txt", -})] -#[test] -fn files_test_paths(input: &Path, output: &Path) { - let input = input.display().to_string(); - let output = output.display().to_string(); - // Check output path is indeed input path with `input` => `output` - assert_eq!(input.replace("input", "output"), output); -} - -/// Can also take slices -#[datatest::files("tests/test-cases", { - input in r"^(.*)\.input\.txt", - output = r"${1}.output.txt", -})] -#[test] -fn files_test_slices(input: &[u8], output: &[u8]) { - let mut actual = b"Hello, ".to_vec(); - actual.extend(input); - actual.push(b'!'); - assert_eq!(actual, output); -} - -fn is_ignore(path: &Path) -> bool { - path.display().to_string().ends_with("case-02.input.txt") -} - -/// Ignore first test case! -#[datatest::files("tests/test-cases", { - input in r"^(.*)\.input\.txt" if !is_ignore, - output = r"${1}.output.txt", -})] -#[test] -fn files_test_ignore(input: &str) { - assert_eq!(input, "Kylie"); -} - -/// Regular tests are also allowed! -#[test] -fn simple_test() { - let palindrome = "never odd or even".replace(' ', ""); - let reversed = palindrome.chars().rev().collect::(); - - assert_eq!(palindrome, reversed) -} - -/// Regular tests are also allowed! Also, could be ignored the same! -#[test] -#[ignore] -fn simple_test_ignored() { - panic!("ignored test!") -} - -/// This test case item does not implement [`std::fmt::Display`], so only line number is shown in -/// the test name. -#[derive(Deserialize)] -struct GreeterTestCase { - name: String, - expected: String, -} - -/// Data-driven tests are defined via `#[datatest::data(..)]` attribute. -/// -/// This attribute specifies a test file with test cases. Currently, the test file have to be in -/// YAML format. This file is deserialized into `Vec`, where `T` is the type of the test function -/// argument (which must implement `serde::Deserialize`). Then, for each element of the vector, a -/// separate test instance is created and executed. -/// -/// Name of each test is derived from the test function module path, test case line number and, -/// optionall, from the [`ToString`] implementation of the test case data (if either [`ToString`] -/// or [`std::fmt::Display`] is implemented). -#[datatest::data("tests/tests.yaml")] -#[test] -fn data_test_line_only(data: &GreeterTestCase) { - assert_eq!(data.expected, format!("Hi, {}!", data.name)); -} - -/// Can take as value, too -#[datatest::data("tests/tests.yaml")] -#[test] -fn data_test_take_owned(mut data: GreeterTestCase) { - data.expected += "boo!"; - data.name += "!boo"; - assert_eq!(data.expected, format!("Hi, {}!", data.name)); -} - -#[ignore] -#[datatest::data("tests/tests.yaml")] -#[test] -fn data_test_line_only_hoplessly_broken(_data: &GreeterTestCase) { - panic!("this test always fails, but this is okay because we marked it as ignored!") -} - -/// This test case item implements [`std::fmt::Display`], which is used to generate test name -#[derive(Deserialize)] -struct GreeterTestCaseNamed { - name: String, - expected: String, -} - -impl fmt::Display for GreeterTestCaseNamed { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(&self.name) - } -} - -#[datatest::data("tests/tests.yaml")] -#[test] -fn data_test_name_and_line(data: &GreeterTestCaseNamed) { - assert_eq!(data.expected, format!("Hi, {}!", data.name)); -} - -/// Can also take string inputs -#[datatest::data("tests/strings.yaml")] -#[test] -fn data_test_string(data: String) { - let half = data.len() / 2; - assert_eq!(data[0..half], data[half..]); -} - -/// Can also use `::datatest::yaml` explicitly -#[datatest::data(::datatest::yaml("tests/strings.yaml"))] -#[test] -fn data_test_yaml(data: String) { - let half = data.len() / 2; - assert_eq!(data[0..half], data[half..]); -} - -// Experimental API: allow custom test cases - -struct StringTestCase { - input: String, - output: String, -} - -fn load_test_cases(path: &str) -> Vec<::datatest::DataTestCaseDesc> { - let input = std::fs::read_to_string(path).unwrap(); - let lines = input.lines().collect::>(); - lines - .chunks(2) - .enumerate() - .map(|(idx, line)| ::datatest::DataTestCaseDesc { - case: StringTestCase { - input: line[0].to_string(), - output: line[1].to_string(), - }, - name: Some(line[0].to_string()), - location: format!("line {}", idx * 2), - }) - .collect() -} - -/// Can have custom deserialization for data tests -#[datatest::data(load_test_cases("tests/cases.txt"))] -#[test] -fn data_test_custom(data: StringTestCase) { - assert_eq!(data.output, format!("Hello, {}!", data.input)); -} +include!("tests/mod.rs"); diff --git a/tests/datatest_stable.rs b/tests/datatest_stable.rs new file mode 100644 index 0000000..4a2ad01 --- /dev/null +++ b/tests/datatest_stable.rs @@ -0,0 +1,6 @@ +#[cfg(feature = "stable")] +include!("tests/mod.rs"); + +fn main() { + datatest::runner(&[]); +} diff --git a/tests/tests/mod.rs b/tests/tests/mod.rs new file mode 100644 index 0000000..b303a35 --- /dev/null +++ b/tests/tests/mod.rs @@ -0,0 +1,205 @@ +use serde::Deserialize; +use std::fmt; +use std::path::Path; + +/// File-driven tests are defined via `#[files(...)]` attribute. +/// +/// The first argument to the attribute is the path to the test data (relative to the crate root +/// directory). +/// +/// The second argument is a block of mappings, each mapping defines the rules of deriving test +/// function arguments. +/// +/// Exactly one mapping should be a "pattern" mapping, defined as ` in ""`. `` +/// is a regular expression applied to every file found in the test directory. For each file path +/// matching the regular expression, test runner will create a new test instance. +/// +/// Other mappings are "template" mappings, they define the template to use for deriving the file +/// paths. Each template have a syntax of a [replacement string] from [`regex`] crate. +/// +/// [replacement string]: https://docs.rs/regex/*/regex/struct.Regex.html#method.replace +/// [regex]: https://docs.rs/regex/*/regex/ +#[datatest::files("tests/test-cases", { +// Pattern is defined via `in` operator. Every file from the `directory` above will be matched +// against this regular expression and every matched file will produce a separate test. +input in r"^(.*)\.input\.txt", +// Template defines a rule for deriving dependent file name based on captures of the pattern. +output = r"${1}.output.txt", +})] +#[test] +fn files_test_strings(input: &str, output: &str) { + assert_eq!(format!("Hello, {}!", input), output); +} + +/// Same as above, but always panics, so marked by `#[ignore]` +#[ignore] +#[datatest::files("tests/test-cases", { +input in r"^(.*)\.input\.txt", +output = r"${1}.output.txt", +})] +#[test] +fn files_tests_not_working_yet_and_never_will(input: &str, output: &str) { + assert_eq!(input, output, "these two will never match!"); +} + +/// Can declare with `&std::path::Path` to get path instead of the content +#[datatest::files("tests/test-cases", { +input in r"^(.*)\.input\.txt", +output = r"${1}.output.txt", +})] +#[test] +fn files_test_paths(input: &Path, output: &Path) { + let input = input.display().to_string(); + let output = output.display().to_string(); + // Check output path is indeed input path with `input` => `output` + assert_eq!(input.replace("input", "output"), output); +} + +/// Can also take slices +#[datatest::files("tests/test-cases", { +input in r"^(.*)\.input\.txt", +output = r"${1}.output.txt", +})] +#[test] +fn files_test_slices(input: &[u8], output: &[u8]) { + let mut actual = b"Hello, ".to_vec(); + actual.extend(input); + actual.push(b'!'); + assert_eq!(actual, output); +} + +fn is_ignore(path: &Path) -> bool { + path.display().to_string().ends_with("case-02.input.txt") +} + +/// Ignore first test case! +#[datatest::files("tests/test-cases", { +input in r"^(.*)\.input\.txt" if !is_ignore, +output = r"${1}.output.txt", +})] +#[test] +fn files_test_ignore(input: &str) { + assert_eq!(input, "Kylie"); +} + +/// Regular tests are also allowed! +#[test] +fn simple_test() { + let palindrome = "never odd or even".replace(' ', ""); + let reversed = palindrome.chars().rev().collect::(); + + assert_eq!(palindrome, reversed) +} + +/// Regular tests are also allowed! Also, could be ignored the same! +#[test] +#[ignore] +fn simple_test_ignored() { + panic!("ignored test!") +} + +/// This test case item does not implement [`std::fmt::Display`], so only line number is shown in +/// the test name. +#[derive(Deserialize)] +struct GreeterTestCase { + name: String, + expected: String, +} + +/// Data-driven tests are defined via `#[datatest::data(..)]` attribute. +/// +/// This attribute specifies a test file with test cases. Currently, the test file have to be in +/// YAML format. This file is deserialized into `Vec`, where `T` is the type of the test function +/// argument (which must implement `serde::Deserialize`). Then, for each element of the vector, a +/// separate test instance is created and executed. +/// +/// Name of each test is derived from the test function module path, test case line number and, +/// optionall, from the [`ToString`] implementation of the test case data (if either [`ToString`] +/// or [`std::fmt::Display`] is implemented). +#[datatest::data("tests/tests.yaml")] +#[test] +fn data_test_line_only(data: &GreeterTestCase) { + assert_eq!(data.expected, format!("Hi, {}!", data.name)); +} + +/// Can take as value, too +#[datatest::data("tests/tests.yaml")] +#[test] +fn data_test_take_owned(mut data: GreeterTestCase) { + data.expected += "boo!"; + data.name += "!boo"; + assert_eq!(data.expected, format!("Hi, {}!", data.name)); +} + +#[ignore] +#[datatest::data("tests/tests.yaml")] +#[test] +fn data_test_line_only_hoplessly_broken(_data: &GreeterTestCase) { + panic!("this test always fails, but this is okay because we marked it as ignored!") +} + +/// This test case item implements [`std::fmt::Display`], which is used to generate test name +#[derive(Deserialize)] +struct GreeterTestCaseNamed { + name: String, + expected: String, +} + +impl fmt::Display for GreeterTestCaseNamed { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(&self.name) + } +} + +#[datatest::data("tests/tests.yaml")] +#[test] +fn data_test_name_and_line(data: &GreeterTestCaseNamed) { + assert_eq!(data.expected, format!("Hi, {}!", data.name)); +} + +/// Can also take string inputs +#[datatest::data("tests/strings.yaml")] +#[test] +fn data_test_string(data: String) { + let half = data.len() / 2; + assert_eq!(data[0..half], data[half..]); +} + +/// Can also use `::datatest::yaml` explicitly +#[datatest::data(::datatest::yaml("tests/strings.yaml"))] +#[test] +fn data_test_yaml(data: String) { + let half = data.len() / 2; + assert_eq!(data[0..half], data[half..]); +} + +// Experimental API: allow custom test cases + +struct StringTestCase { + input: String, + output: String, +} + +fn load_test_cases(path: &str) -> Vec<::datatest::DataTestCaseDesc> { + let input = std::fs::read_to_string(path).unwrap(); + let lines = input.lines().collect::>(); + lines + .chunks(2) + .enumerate() + .map(|(idx, line)| ::datatest::DataTestCaseDesc { + case: StringTestCase { + input: line[0].to_string(), + output: line[1].to_string(), + }, + name: Some(line[0].to_string()), + location: format!("line {}", idx * 2), + }) + .collect() +} + +/// Can have custom deserialization for data tests +#[datatest::data(load_test_cases("tests/cases.txt"))] +#[test] +fn data_test_custom(data: StringTestCase) { + assert_eq!(data.output, format!("Hello, {}!", data.input)); +} diff --git a/tests/unicode.rs b/tests/unicode.rs index 4ebd764..4cd43dc 100644 --- a/tests/unicode.rs +++ b/tests/unicode.rs @@ -1,3 +1,4 @@ +#![cfg(feature = "nightly")] #![feature(non_ascii_idents)] #![feature(custom_test_frameworks)] #![test_runner(datatest::runner)]