From 2fe0b28fc318429e572c9e4bb3241e6b63d08932 Mon Sep 17 00:00:00 2001 From: Ivan Dubrov Date: Sat, 17 Aug 2019 14:53:51 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=A5=20baby=20steps=20towards=20stable?= =?UTF-8?q?=20Rust?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Started working on #4 (support for stable Rust). First issue we need to solve is to get access to the harness (since we don't really want to implement it ourselves). There is https://crates.io/crates/libtest crate, which is recent version of Rust internal test harness, extracted as a crate. However, it only compiles on nightly, so it won't help us here. There is also https://crates.io/crates/rustc-test, but it is 2 years old. I haven't checked its features, but might not support some of the desired functionality (like, JSON output in tests? colored output?). So, the third option (which I'm using here) is to use `test` crate from the Rust itself and also set `RUSTC_BOOTSTRAP=1` for our crate so we can access it on stable channel. Not great, but works for now. Second issue is to get access to the tests. On nightly, we use `#[test_case]` to hijack Rust tests registration so we can get access to them in nightly. Cannot do that on stable. What would help here is something along the lines of https://internals.rust-lang.org/t/idea-global-static-variables-extendable-at-compile-time/9879 or https://internals.rust-lang.org/t/pre-rfc-add-language-support-for-global-constructor-functions. Don't have that, so we use https://crates.io/crates/ctor crate to build our own registry of tests, similar to https://crates.io/crates/inventory. The caveat here is potentially hitting https://github.com/dtolnay/inventory/issues/7 issue which would manifest itself as test being silently ignored. Not great, but let's see how bad it will be. Third piece of the puzzle is to intercept execution of tests. This is done by asking users to use `harness = false` in their `Cargo.toml`, in which case we take full control of test execution. Finally, the last challenge is that with `harness = false`, we don't have a good way to intercept "standard" tests (`#[test]`): https://users.rust-lang.org/t/capturing-test-when-harness-false-in-cargo-toml/28115 So, the plan here is to provide `#[datatest::test]` attribute that will behave similar to built-in `#[test]` attribute, but will use our own registry for tests. No need to support `#[bench]` as it is not supported on stable channel anyway. The caveat in this case is that if you use built-in `#[test]`, your test will be silently ignored. Not great, not sure what to do about it. Proper solution, of course, would be driving RFC for custom test frameworks: https://github.com/rust-lang/rust/issues/50297 😅 --- Cargo.toml | 4 + build.rs | 2 +- datatest-derive/src/lib.rs | 55 +++++++---- src/lib.rs | 5 + src/runner.rs | 73 ++++++++++---- tests/datatest.rs | 198 +------------------------------------ tests/datatest_stable.rs | 6 ++ tests/tests/mod.rs | 192 +++++++++++++++++++++++++++++++++++ 8 files changed, 302 insertions(+), 233 deletions(-) create mode 100644 tests/datatest_stable.rs create mode 100644 tests/tests/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 7e7fa4e..56acd35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,10 @@ description = """ Data-driven tests in Rust """ +[[test]] +name = "datatest_stable" +harness = false + [build-dependencies] version_check = "0.9.1" diff --git a/build.rs b/build.rs index d671658..91b4f22 100644 --- a/build.rs +++ b/build.rs @@ -8,4 +8,4 @@ fn main() { println!("cargo:rustc-cfg=feature=\"stable\""); } println!("cargo:rustc-env=RUSTC_BOOTSTRAP=1"); -} \ No newline at end of file +} diff --git a/datatest-derive/src/lib.rs b/datatest-derive/src/lib.rs index 0fb8b00..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; @@ -253,14 +248,15 @@ fn files_internal( let orig_func_name = &func_item.ident; let (kind, bencher_param) = if info.bench { - (quote!(BenchFn), quote!(bencher: &mut ::datatest::__internal::Bencher,)) + ( + quote!(BenchFn), + quote!(bencher: &mut ::datatest::__internal::Bencher,), + ) } else { (quote!(TestFn), quote!()) }; - let registration = test_registration(channel); - - // Adding `#[allow(unused_attributes)]` to `#orig_func` to allow `#[ignore]` attribute + let registration = test_registration(channel, &desc_ident); let output = quote! { #registration #[automatically_derived] @@ -347,14 +343,13 @@ impl Parse for DataTestArgs { } } - /// Wrapper that turns on behavior that works on stable Rust. #[proc_macro_attribute] pub fn data_stable( args: proc_macro::TokenStream, func: proc_macro::TokenStream, ) -> proc_macro::TokenStream { - files_internal(args, func, Channel::Stable) + data_internal(args, func, Channel::Stable) } /// Wrapper that turns on behavior that works only on nightly Rust. @@ -374,7 +369,7 @@ fn data_internal( let mut func_item = parse_macro_input!(func as ItemFn); let cases: DataTestArgs = parse_macro_input!(args as DataTestArgs); let cases = match cases { - DataTestArgs::Literal(path) => quote!(::datatest::yaml(#path)), + DataTestArgs::Literal(path) => quote!(datatest::yaml(#path)), DataTestArgs::Expression(expr) => quote!(#expr), }; @@ -430,8 +425,7 @@ fn data_internal( ) }; - let registration = test_registration(channel); - + let registration = test_registration(channel, &desc_ident); let output = quote! { #registration #[automatically_derived] @@ -472,7 +466,28 @@ fn data_internal( output.into() } - -fn test_registration(channel: Channel) -> TokenStream { - quote!(#[test_case]) -} \ No newline at end of file +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 7d7284c..a939373 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -115,12 +115,17 @@ mod data; mod files; mod runner; +/// Internal re-exports for the procedural macro to use. #[doc(hidden)] 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; diff --git a/src/runner.rs b/src/runner.rs index 35047b5..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(|_| { @@ -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,6 +299,30 @@ 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; } @@ -296,7 +333,7 @@ impl Termination for () { } } -impl Termination for Result { +impl Termination for Result { fn is_success(&self) -> bool { self.is_ok() } diff --git a/tests/datatest.rs b/tests/datatest.rs index 1858e3d..92194c9 100644 --- a/tests/datatest.rs +++ b/tests/datatest.rs @@ -1,195 +1,5 @@ -#![cfg_attr(feature = "nightly", feature(custom_test_frameworks))] -#![cfg_attr(feature = "nightly", test_runner(datatest::runner))] +#![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, -} - -impl fmt::Display for GreeterTestCase { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(&self.name) - } -} - -/// 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!") -} - -/// 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..d126b81 --- /dev/null +++ b/tests/tests/mod.rs @@ -0,0 +1,192 @@ +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, +} + +impl fmt::Display for GreeterTestCase { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(&self.name) + } +} + +/// 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!") +} + +/// 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)); +}