From 43a5e8b76ca12f1e21a32c53421ca7fb16cc1804 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Sun, 9 Mar 2025 08:57:35 -0400 Subject: [PATCH 1/5] feat: multiple runners --- examples/shapes/polytest.toml | 6 +- src/main.rs | 195 +++++++++++++++++++++------------- 2 files changed, 124 insertions(+), 77 deletions(-) diff --git a/examples/shapes/polytest.toml b/examples/shapes/polytest.toml index 87bfb11..c46d271 100644 --- a/examples/shapes/polytest.toml +++ b/examples/shapes/polytest.toml @@ -80,12 +80,12 @@ test_regex_template = "def test_{{ name | convert_case('Snake') }}" template_dir = "./templates/minitest_unit" -[custom_target.minitest_unit.runner] +[[custom_target.minitest_unit.runners]] +command = "bundle exec rake test A='--verbose'" + fail_regex_template = "Test{{ suite_name | convert_case('Pascal') }}#test_{{ test_name }} = \\d+\\.\\d+ s = (F|E)" pass_regex_template = "Test{{ suite_name | convert_case('Pascal') }}#test_{{ test_name }} = \\d+\\.\\d+ s = \\." -command = "bundle exec rake test A='--verbose'" - # Custom Document [document.test_cases_csv] diff --git a/src/main.rs b/src/main.rs index 41d8e34..97315f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -99,7 +99,7 @@ struct Runner { fail_regex_template: String, pass_regex_template: String, env: Option>, - work_dir: PathBuf + work_dir: PathBuf, } impl Runner { @@ -109,7 +109,7 @@ impl Runner { fail_regex_template: "(?m)".to_owned() + config.fail_regex_template.as_str(), pass_regex_template: "(?m)".to_owned() + config.pass_regex_template.as_str(), env: config.env.clone(), - work_dir: config.work_dir.clone().unwrap_or_else(|| out_dir.to_path_buf()), + work_dir: config.work_dir.clone().unwrap_or(out_dir.to_path_buf()), } } } @@ -123,7 +123,7 @@ struct Target { suite_template: String, group_template: String, test_template: String, - runner: Runner, + runners: Vec, } #[derive(Deserialize, Debug, Clone)] @@ -177,12 +177,8 @@ fn find_template_file(template_dir: &Path, template_name: &str) -> Result Result { - return match id { + fn from_config(config: &TargetConfig, id: &str, config_root: &Path) -> Result { + return match id { "pytest" => { Ok(Self { id: id.to_string(), @@ -198,58 +194,67 @@ impl Target { .to_string(), test_template: include_str!("../templates/pytest/test.py.jinja") .to_string(), - runner: Runner { + runners: vec![Runner { env: None, command: "pytest -v".to_string(), fail_regex_template: r"(?m){{ file_name }}::test_{{ test_name }} FAILED".to_string(), pass_regex_template: r"(?m){{ file_name }}::test_{{ test_name }} PASSED".to_string(), - work_dir: config_root.join(&config.out_dir), - }, + work_dir: config_root.join(&config.out_dir), + },] }) } "bun" => { Ok(Self { id: id.to_string(), test_regex_template: r#"(?m)test\("{{ name }}","#.to_string(), - suite_file_name_template: + suite_file_name_template: "{{ suite.name | convert_case('Snake') }}.test.ts".to_string(), out_dir: config_root.join(&config.out_dir), - suite_template: + suite_template: include_str!("../templates/bun/suite.ts.jinja").to_string(), - group_template: + group_template: include_str!("../templates/bun/group.ts.jinja").to_string(), test_template: include_str!("../templates/bun/test.ts.jinja").to_string(), - runner: Runner { + runners: vec![Runner { env: None, command: "bun test".to_string(), fail_regex_template: r"(?m)\(fail\) {{ suite_name }} > {{ group_name }} > {{ test_name }}( \[\d+\.\d+ms])*$".to_string(), pass_regex_template: r"(?m)\(pass\) {{ suite_name }} > {{ group_name }} > {{ test_name }}( \[\d+\.\d+ms])*$".to_string(), - work_dir: config_root.join(&config.out_dir), - }, + work_dir: config_root.join(&config.out_dir), + },] }) } _ => { Err(anyhow!("config defined for target {} but this is not a supported target. Perhaps you meant to use custom_target?", id)) } - } - } + }; + } - pub fn from_custom_config(config: &CustomTargetConfig, id: &str, config_root: &Path) -> Result { + pub fn from_custom_config( + config: &CustomTargetConfig, + id: &str, + config_root: &Path, + ) -> Result { let template_dir = &config_root.join(&config.template_dir); - let suite_file = find_template_file(template_dir, "suite*").context(format!("failed to find suite template for {}", id))?; - let suite_template = std::fs::read_to_string(suite_file).context(format!("failed to read suite template file for {}", id))?; + let suite_file = find_template_file(template_dir, "suite*") + .context(format!("failed to find suite template for {}", id))?; + let suite_template = std::fs::read_to_string(suite_file) + .context(format!("failed to read suite template file for {}", id))?; - let group_file = find_template_file(template_dir, "group*").context(format!("failed to find group template for {}", id))?; - let group_template = std::fs::read_to_string(group_file).context(format!("failed to read group template file for {}", id))?; + let group_file = find_template_file(template_dir, "group*") + .context(format!("failed to find group template for {}", id))?; + let group_template = std::fs::read_to_string(group_file) + .context(format!("failed to read group template file for {}", id))?; - let test_file = find_template_file(template_dir, "test*").context(format!("failed to find test template for {}", id))?; - let test_template = std::fs::read_to_string(test_file).context(format!("failed to read test template file for {}", id))?; + let test_file = find_template_file(template_dir, "test*") + .context(format!("failed to find test template for {}", id))?; + let test_template = std::fs::read_to_string(test_file) + .context(format!("failed to read test template file for {}", id))?; - - Ok(Self { + Ok(Self { id: id.to_string(), test_regex_template: "(?m)".to_owned() + config.test_regex_template.as_str(), out_dir: config_root.join(&config.out_dir), @@ -257,7 +262,13 @@ impl Target { suite_template, group_template, test_template, - runner: Runner::from_config(&config.runner, &config_root.join(&config.out_dir)), + runners: config + .runners + .iter() + .map(|runner_cfg| { + Runner::from_config(runner_cfg, &config_root.join(&config.out_dir)) + }) + .collect(), }) } } @@ -294,7 +305,7 @@ struct CustomTargetConfig { test_regex_template: String, suite_file_name_template: String, template_dir: PathBuf, - runner: RunnerConfig, + runners: Vec, } #[derive(Deserialize, Debug, Clone)] @@ -351,7 +362,11 @@ impl Group { fn from_config(group_config: &GroupConfig, group_id: &str) -> Self { Self { name: group_id.to_string(), - tests: group_config.tests.iter().map(|(id, t)| Test::from_config(t, id)).collect(), + tests: group_config + .tests + .iter() + .map(|(id, t)| Test::from_config(t, id)) + .collect(), desc: group_config.desc.clone().unwrap_or("".to_string()), } } @@ -464,15 +479,18 @@ fn main() -> Result<()> { .collect::>>()?; for target in &all_targets { + for runner in &target.runners { + let target_cmd = target.id.clone() + &runner.command; - templates.insert( - format!("{}_fail_regex", target.id), - target.runner.fail_regex_template.clone(), - ); - templates.insert( - format!("{}_pass_regex", target.id), - target.runner.pass_regex_template.clone(), - ); + templates.insert( + format!("{}_fail_regex", target_cmd), + runner.fail_regex_template.clone(), + ); + templates.insert( + format!("{}_pass_regex", target_cmd), + runner.pass_regex_template.clone(), + ); + } templates.insert( format!("{}_suite_file_name", target.id), @@ -484,11 +502,20 @@ fn main() -> Result<()> { target.test_regex_template.to_string(), ); - templates.insert(format!("{}_suite", target.id), target.suite_template.to_string()); + templates.insert( + format!("{}_suite", target.id), + target.suite_template.to_string(), + ); - templates.insert(format!("{}_group", target.id), target.group_template.to_string()); + templates.insert( + format!("{}_group", target.id), + target.group_template.to_string(), + ); - templates.insert(format!("{}_test", target.id), target.test_template.to_string()); + templates.insert( + format!("{}_test", target.id), + target.test_template.to_string(), + ); } templates.iter().for_each(|(name, template)| { @@ -533,7 +560,7 @@ fn main() -> Result<()> { } } Commands::Run(run) => { - let mut statuses = IndexMap::::new(); + let mut statuses = IndexMap::<(String, String), ExitStatus>::new(); let mut outputs = HashMap::::new(); let targets = match run.target { @@ -546,41 +573,49 @@ fn main() -> Result<()> { }; for target in &targets { - let runner = target.runner.clone(); + for runner in &target.runners { + println!("Running {}: {}", target.id, runner.command); - println!("Running {}: {}", target.id, runner.command); + let parsed_cmd: Vec = + shlex::Shlex::new(runner.command.as_str()).collect(); - let parsed_cmd: Vec = shlex::Shlex::new(runner.command.as_str()).collect(); - let mut runner_cmd = cmd(&parsed_cmd[0], &parsed_cmd[1..]).dir(&target.out_dir); - runner_cmd = runner_cmd.dir(&target.runner.work_dir); - if let Some(env) = runner.env { - for (key, value) in env { - runner_cmd = runner_cmd.env(key, value); + let mut runner_cmd = + cmd(&parsed_cmd[0], &parsed_cmd[1..]).dir(&runner.work_dir); + + if let Some(env) = &runner.env { + for (key, value) in env { + runner_cmd = runner_cmd.env(key, value); + } } - } - let runner_result = runner_cmd.unchecked(); - let reader = runner_result.stderr_to_stdout().reader()?; - let output = &mut String::new(); - BufReader::new(reader).read_to_string(output)?; + let runner_result = runner_cmd.unchecked(); + let reader = runner_result.stderr_to_stdout().reader()?; + let output = &mut String::new(); + BufReader::new(reader).read_to_string(output)?; - outputs.insert(target.id.clone(), output.clone()); - statuses.insert(target.id.clone(), runner_result.run()?.status); + outputs.insert(target.id.clone() + &runner.command, output.clone()); + statuses.insert( + (target.id.clone(), runner.command.clone()), + runner_result.run()?.status, + ); + } } - for (target_id, status) in statuses { + for ((target_id, command), status) in statuses { + let target_cmd = target_id.clone() + &command; + let target = targets .iter() .find(|t| t.id == target_id) .expect("the target should exist because the status exists"); let fail_regex_template = env - .get_template(format!("{}_fail_regex", target_id).as_str()) + .get_template(format!("{}_fail_regex", target_cmd).as_str()) .expect("template should exist since it was just added above"); let pass_regex_template = env - .get_template(format!("{}_pass_regex", target_id).as_str()) - .context(format!("could not find test pass regex for {}", target_id))?; + .get_template(format!("{}_pass_regex", target_cmd).as_str()) + .expect("template should exist since it was just added above"); let mut fails: Vec = Vec::new(); @@ -606,16 +641,16 @@ fn main() -> Result<()> { }) .context(format!( "failed to render fail regex for {}", - target_id + target_cmd ))?; let fail_regex = Regex::new(&fail_regex).unwrap(); - if fail_regex.is_match(&outputs[&target_id]) { + if fail_regex.is_match(&outputs[&target_cmd]) { fails.push( format!( - " {} > {} > {} > {}: FAILED", - target_id, suite.name, group.name, test.name + " {} ({}) > {} > {} > {}: FAILED", + target_id, command, suite.name, group.name, test.name ) .to_string(), ); @@ -629,16 +664,20 @@ fn main() -> Result<()> { }) .context(format!( "failed to render pass regex for {}", - target_id + target_cmd ))?; let pass_regex = Regex::new(&pass_regex).unwrap(); - if !pass_regex.is_match(&outputs[&target_id]) { + if !pass_regex.is_match(&outputs[&target_cmd]) { fails.push( format!( - " {} > {} > {} > {}: UNKNOWN", - target_id, suite.name, group.name, test.name + " {} ({}) > {} > {} > {}: UNKNOWN", + target_id, + command, + suite.name, + group.name, + test.name ) .to_string(), ); @@ -650,9 +689,12 @@ fn main() -> Result<()> { } // If the command was exit 0 AND there were no FAILED or UNKNOWN test results if status.success() && fails.is_empty() { - println!("{} ran succesfully!", target_id); + println!("{} ({}) ran succesfully!", target_id, command); } else { - eprintln!("{} failed to run succesfully ({})", target_id, status); + eprintln!( + "{} ({}) failed to run succesfully ({})", + target_id, command, status + ); for failure in &fails { eprintln!("{failure}") } @@ -995,7 +1037,12 @@ fn generate_document( let test_values: Vec = config .groups .iter() - .flat_map(|(_, g_cfg)| g_cfg.tests.iter().map(|(t_id, t_cfg)| Test::from_config(t_cfg, t_id))) + .flat_map(|(_, g_cfg)| { + g_cfg + .tests + .iter() + .map(|(t_id, t_cfg)| Test::from_config(t_cfg, t_id)) + }) .collect(); let template = env From b1bcbed4e8aa4b8eb4bacd7ce31ef78c02a9531d Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Sun, 9 Mar 2025 09:43:58 -0400 Subject: [PATCH 2/5] feat: use table for runners --- examples/shapes/polytest.toml | 2 +- src/main.rs | 111 ++++++++++++++++++++-------------- 2 files changed, 65 insertions(+), 48 deletions(-) diff --git a/examples/shapes/polytest.toml b/examples/shapes/polytest.toml index c46d271..f7515bc 100644 --- a/examples/shapes/polytest.toml +++ b/examples/shapes/polytest.toml @@ -80,7 +80,7 @@ test_regex_template = "def test_{{ name | convert_case('Snake') }}" template_dir = "./templates/minitest_unit" -[[custom_target.minitest_unit.runners]] +[custom_target.minitest_unit.runner."rake test"] command = "bundle exec rake test A='--verbose'" fail_regex_template = "Test{{ suite_name | convert_case('Pascal') }}#test_{{ test_name }} = \\d+\\.\\d+ s = (F|E)" diff --git a/src/main.rs b/src/main.rs index 97315f6..1cb0da8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -123,7 +123,7 @@ struct Target { suite_template: String, group_template: String, test_template: String, - runners: Vec, + runners: IndexMap, } #[derive(Deserialize, Debug, Clone)] @@ -180,6 +180,20 @@ impl Target { fn from_config(config: &TargetConfig, id: &str, config_root: &Path) -> Result { return match id { "pytest" => { + let mut runners: IndexMap = IndexMap::new(); + + let default_runner = Runner { + env: None, + command: "pytest -v".to_string(), + fail_regex_template: + r"(?m){{ file_name }}::test_{{ test_name }} FAILED".to_string(), + pass_regex_template: + r"(?m){{ file_name }}::test_{{ test_name }} PASSED".to_string(), + work_dir: config_root.join(&config.out_dir), + }; + + runners.insert("pytest -v".to_string(), default_runner); + Ok(Self { id: id.to_string(), test_regex_template: r"(?m)def test_{{ name | convert_case('Snake') }}\(" @@ -194,18 +208,22 @@ impl Target { .to_string(), test_template: include_str!("../templates/pytest/test.py.jinja") .to_string(), - runners: vec![Runner { - env: None, - command: "pytest -v".to_string(), - fail_regex_template: - r"(?m){{ file_name }}::test_{{ test_name }} FAILED".to_string(), - pass_regex_template: - r"(?m){{ file_name }}::test_{{ test_name }} PASSED".to_string(), - work_dir: config_root.join(&config.out_dir), - },] + runners, }) } "bun" => { + let mut runners: IndexMap = IndexMap::new(); + + let default_runner = Runner { + env: None, + command: "bun test".to_string(), + fail_regex_template: r"(?m)\(fail\) {{ suite_name }} > {{ group_name }} > {{ test_name }}( \[\d+\.\d+ms])*$".to_string(), + pass_regex_template: r"(?m)\(pass\) {{ suite_name }} > {{ group_name }} > {{ test_name }}( \[\d+\.\d+ms])*$".to_string(), + work_dir: config_root.join(&config.out_dir), + }; + + runners.insert("bun test".to_string(), default_runner); + Ok(Self { id: id.to_string(), test_regex_template: r#"(?m)test\("{{ name }}","#.to_string(), @@ -217,14 +235,8 @@ impl Target { group_template: include_str!("../templates/bun/group.ts.jinja").to_string(), test_template: include_str!("../templates/bun/test.ts.jinja").to_string(), - runners: vec![Runner { - env: None, - command: "bun test".to_string(), - fail_regex_template: r"(?m)\(fail\) {{ suite_name }} > {{ group_name }} > {{ test_name }}( \[\d+\.\d+ms])*$".to_string(), - pass_regex_template: r"(?m)\(pass\) {{ suite_name }} > {{ group_name }} > {{ test_name }}( \[\d+\.\d+ms])*$".to_string(), - work_dir: config_root.join(&config.out_dir), - },] - }) + runners, + }) } _ => { Err(anyhow!("config defined for target {} but this is not a supported target. Perhaps you meant to use custom_target?", id)) @@ -254,6 +266,15 @@ impl Target { let test_template = std::fs::read_to_string(test_file) .context(format!("failed to read test template file for {}", id))?; + let mut runners: IndexMap = IndexMap::new(); + + config.runners.iter().for_each(|(runner_id, runner_cfg)| { + runners.insert( + runner_id.clone(), + Runner::from_config(runner_cfg, &config_root.join(&config.out_dir)), + ); + }); + Ok(Self { id: id.to_string(), test_regex_template: "(?m)".to_owned() + config.test_regex_template.as_str(), @@ -262,13 +283,7 @@ impl Target { suite_template, group_template, test_template, - runners: config - .runners - .iter() - .map(|runner_cfg| { - Runner::from_config(runner_cfg, &config_root.join(&config.out_dir)) - }) - .collect(), + runners, }) } } @@ -305,7 +320,9 @@ struct CustomTargetConfig { test_regex_template: String, suite_file_name_template: String, template_dir: PathBuf, - runners: Vec, + + #[serde(rename = "runner")] + runners: IndexMap, } #[derive(Deserialize, Debug, Clone)] @@ -479,15 +496,15 @@ fn main() -> Result<()> { .collect::>>()?; for target in &all_targets { - for runner in &target.runners { - let target_cmd = target.id.clone() + &runner.command; + for (runner_id, runner) in &target.runners { + let target_runner = target.id.clone() + &runner_id; templates.insert( - format!("{}_fail_regex", target_cmd), + format!("{}_fail_regex", target_runner), runner.fail_regex_template.clone(), ); templates.insert( - format!("{}_pass_regex", target_cmd), + format!("{}_pass_regex", target_runner), runner.pass_regex_template.clone(), ); } @@ -573,8 +590,8 @@ fn main() -> Result<()> { }; for target in &targets { - for runner in &target.runners { - println!("Running {}: {}", target.id, runner.command); + for (runner_id, runner) in &target.runners { + println!("Running {} > {}: {}", target.id, runner_id, runner.command); let parsed_cmd: Vec = shlex::Shlex::new(runner.command.as_str()).collect(); @@ -593,16 +610,16 @@ fn main() -> Result<()> { let output = &mut String::new(); BufReader::new(reader).read_to_string(output)?; - outputs.insert(target.id.clone() + &runner.command, output.clone()); + outputs.insert(target.id.clone() + &runner_id, output.clone()); statuses.insert( - (target.id.clone(), runner.command.clone()), + (target.id.clone(), runner_id.clone()), runner_result.run()?.status, ); } } - for ((target_id, command), status) in statuses { - let target_cmd = target_id.clone() + &command; + for ((target_id, runner_id), status) in statuses { + let target_runner = target_id.clone() + &runner_id; let target = targets .iter() @@ -610,11 +627,11 @@ fn main() -> Result<()> { .expect("the target should exist because the status exists"); let fail_regex_template = env - .get_template(format!("{}_fail_regex", target_cmd).as_str()) + .get_template(format!("{}_fail_regex", target_runner).as_str()) .expect("template should exist since it was just added above"); let pass_regex_template = env - .get_template(format!("{}_pass_regex", target_cmd).as_str()) + .get_template(format!("{}_pass_regex", target_runner).as_str()) .expect("template should exist since it was just added above"); let mut fails: Vec = Vec::new(); @@ -641,16 +658,16 @@ fn main() -> Result<()> { }) .context(format!( "failed to render fail regex for {}", - target_cmd + target_runner ))?; let fail_regex = Regex::new(&fail_regex).unwrap(); - if fail_regex.is_match(&outputs[&target_cmd]) { + if fail_regex.is_match(&outputs[&target_runner]) { fails.push( format!( " {} ({}) > {} > {} > {}: FAILED", - target_id, command, suite.name, group.name, test.name + target_id, runner_id, suite.name, group.name, test.name ) .to_string(), ); @@ -664,17 +681,17 @@ fn main() -> Result<()> { }) .context(format!( "failed to render pass regex for {}", - target_cmd + target_runner ))?; let pass_regex = Regex::new(&pass_regex).unwrap(); - if !pass_regex.is_match(&outputs[&target_cmd]) { + if !pass_regex.is_match(&outputs[&target_runner]) { fails.push( format!( " {} ({}) > {} > {} > {}: UNKNOWN", target_id, - command, + runner_id, suite.name, group.name, test.name @@ -689,11 +706,11 @@ fn main() -> Result<()> { } // If the command was exit 0 AND there were no FAILED or UNKNOWN test results if status.success() && fails.is_empty() { - println!("{} ({}) ran succesfully!", target_id, command); + println!("{} ({}): ran succesfully!", target_id, runner_id); } else { eprintln!( - "{} ({}) failed to run succesfully ({})", - target_id, command, status + "{} ({}): failed to run succesfully ({})", + target_id, runner_id, status ); for failure in &fails { eprintln!("{failure}") From 51bc42e2afdc6e2d746bf68d5a481d5a70ede98c Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Sun, 9 Mar 2025 09:48:52 -0400 Subject: [PATCH 3/5] docs: update config docs --- docs/configuration.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 88acaa3..7996928 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -146,15 +146,19 @@ The `template_dir` field is used to define the directory that contains the templ See [templates documentation](./templates.md) for more information on the expected contents of these files and available variables. -## custom_target.\.runner +## custom_target.\.runner.\ -`runner` is a table that defines how to run the test suites and parse the results. +`runner` is a table that defines how to run the test suites and parse the results. There can be multiple runners defined for one target (for example, testing multiple platforms) ### Fields #### command -The `command` field is used to define the command to run the test. +The `command` field is used to define the command to run the tests. + +#### work_dir + +The `work_dir` field is used to define the working directory for the runner. If not defined, the `out_dir` of the target will be used. #### fail_regex_template @@ -182,6 +186,16 @@ The variables available for use in the template. See [templates documentation](. * `group_name` - The name of the group that contains the test (i.e. for `group.some_group`, `some_group` ) * `test_name` - The name of the test (i.e. for `test.some_test`, `some_test` ) +### Example + +```toml +[custom_target.minitest_unit.runner."rake test"] +command = "bundle exec rake test A='--verbose'" + +fail_regex_template = "Test{{ suite_name | convert_case('Pascal') }}#test_{{ test_name }} = \\d+\\.\\d+ s = (F|E)" +pass_regex_template = "Test{{ suite_name | convert_case('Pascal') }}#test_{{ test_name }} = \\d+\\.\\d+ s = \\." +``` + ## document.\ A document is a generated file that is regenered each time `polytest generate` is ran. From fb25ab4d52349624aef9d905bc1385b3523dc570 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Sun, 9 Mar 2025 11:54:24 -0400 Subject: [PATCH 4/5] feat: inherit runner config values from previous runner --- src/main.rs | 137 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 93 insertions(+), 44 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1cb0da8..19010e9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -84,11 +84,11 @@ impl ConfigMeta { } } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone, Default)] struct RunnerConfig { - command: String, - fail_regex_template: String, - pass_regex_template: String, + command: Option, + fail_regex_template: Option, + pass_regex_template: Option, env: Option>, work_dir: Option, } @@ -103,17 +103,64 @@ struct Runner { } impl Runner { - fn from_config(config: &RunnerConfig, out_dir: &Path) -> Self { - Self { - command: config.command.clone(), - fail_regex_template: "(?m)".to_owned() + config.fail_regex_template.as_str(), - pass_regex_template: "(?m)".to_owned() + config.pass_regex_template.as_str(), - env: config.env.clone(), - work_dir: config.work_dir.clone().unwrap_or(out_dir.to_path_buf()), - } + fn from_configs( + default_configs: IndexMap, + configs: &IndexMap, + out_dir: &Path, + ) -> Result> { + let mut prev: Option = None; + + let mut runners: IndexMap = IndexMap::new(); + + default_configs + .iter() + .chain(configs.iter()) + .map(|(id, cfg)| { + println!("{}, {:?}", id, configs); + let fail_regex = cfg + .fail_regex_template + .clone() + .or_else(|| prev.clone().and_then(|p| p.fail_regex_template)) + .context(format!( + "fail_regex_template not defined for runner: {}", + id + ))?; + + let pass_regex = cfg + .pass_regex_template + .clone() + .or_else(|| prev.clone().and_then(|p| p.pass_regex_template)) + .context(format!( + "pass_regex_template not defined for runner: {}", + id + ))?; + + let runner = Runner { + command: cfg + .command + .clone() + .or_else(|| prev.clone().and_then(|p| p.command)) + .context(format!("command not defined for runner: {}", id))?, + fail_regex_template: "(?m)".to_owned() + &fail_regex, + pass_regex_template: "(?m)".to_owned() + &pass_regex, + env: cfg.env.clone().or_else(|| prev.clone().and_then(|p| p.env)), + work_dir: cfg + .work_dir + .clone() + .or_else(|| prev.clone().and_then(|p| p.work_dir)) + .unwrap_or(out_dir.to_owned()), + }; + + runners.insert(id.clone(), runner); + + prev = Some(cfg.clone()); + Ok(()) + }) + .collect::>()?; + + Ok(runners) } } - #[derive(Deserialize, Debug, Clone)] struct Target { id: String, @@ -180,25 +227,22 @@ impl Target { fn from_config(config: &TargetConfig, id: &str, config_root: &Path) -> Result { return match id { "pytest" => { - let mut runners: IndexMap = IndexMap::new(); + let mut default_runner_cfgs: IndexMap = IndexMap::new(); - let default_runner = Runner { + default_runner_cfgs.insert("pytest -v".to_string(), RunnerConfig { env: None, - command: "pytest -v".to_string(), + command: Some("pytest -v".to_string()), fail_regex_template: - r"(?m){{ file_name }}::test_{{ test_name }} FAILED".to_string(), + Some("{{ file_name }}::test_{{ test_name }} FAILED".to_string()), pass_regex_template: - r"(?m){{ file_name }}::test_{{ test_name }} PASSED".to_string(), - work_dir: config_root.join(&config.out_dir), - }; - - runners.insert("pytest -v".to_string(), default_runner); + Some("{{ file_name }}::test_{{ test_name }} PASSED".to_string()), + work_dir: Some(config.out_dir.clone()), + }); Ok(Self { id: id.to_string(), test_regex_template: r"(?m)def test_{{ name | convert_case('Snake') }}\(" .to_string(), - suite_file_name_template: "test_{{ suite.name | convert_case('Snake') }}.py".to_string(), out_dir: config_root.join(&config.out_dir), @@ -208,21 +252,24 @@ impl Target { .to_string(), test_template: include_str!("../templates/pytest/test.py.jinja") .to_string(), - runners, + runners: Runner::from_configs( + default_runner_cfgs, + &config.runners.clone().unwrap_or_default(), + &config_root.join(&config.out_dir) + )?, }) } "bun" => { - let mut runners: IndexMap = IndexMap::new(); - let default_runner = Runner { - env: None, - command: "bun test".to_string(), - fail_regex_template: r"(?m)\(fail\) {{ suite_name }} > {{ group_name }} > {{ test_name }}( \[\d+\.\d+ms])*$".to_string(), - pass_regex_template: r"(?m)\(pass\) {{ suite_name }} > {{ group_name }} > {{ test_name }}( \[\d+\.\d+ms])*$".to_string(), - work_dir: config_root.join(&config.out_dir), - }; + let mut default_runner_cfgs: IndexMap = IndexMap::new(); - runners.insert("bun test".to_string(), default_runner); + default_runner_cfgs.insert("bun test".to_string(), RunnerConfig { + env: None, + command: Some("bun test".to_string()), + fail_regex_template: Some(r"\(fail\) {{ suite_name }} > {{ group_name }} > {{ test_name }}( \[\d+\.\d+ms])*$".to_string()), + pass_regex_template: Some(r"\(pass\) {{ suite_name }} > {{ group_name }} > {{ test_name }}( \[\d+\.\d+ms])*$".to_string()), + work_dir: Some(config.out_dir.clone()), + }); Ok(Self { id: id.to_string(), @@ -235,7 +282,11 @@ impl Target { group_template: include_str!("../templates/bun/group.ts.jinja").to_string(), test_template: include_str!("../templates/bun/test.ts.jinja").to_string(), - runners, + runners: Runner::from_configs( + default_runner_cfgs, + &config.runners.clone().unwrap_or_default(), + &config_root.join(&config.out_dir) + )?, }) } _ => { @@ -266,15 +317,6 @@ impl Target { let test_template = std::fs::read_to_string(test_file) .context(format!("failed to read test template file for {}", id))?; - let mut runners: IndexMap = IndexMap::new(); - - config.runners.iter().for_each(|(runner_id, runner_cfg)| { - runners.insert( - runner_id.clone(), - Runner::from_config(runner_cfg, &config_root.join(&config.out_dir)), - ); - }); - Ok(Self { id: id.to_string(), test_regex_template: "(?m)".to_owned() + config.test_regex_template.as_str(), @@ -283,7 +325,11 @@ impl Target { suite_template, group_template, test_template, - runners, + runners: Runner::from_configs( + IndexMap::::default(), + &config.runners, + &config_root.join(&config.out_dir), + )?, }) } } @@ -311,6 +357,9 @@ struct Config { #[derive(Deserialize, Debug, Clone)] struct TargetConfig { out_dir: PathBuf, + + #[serde(rename = "runner")] + runners: Option>, } #[derive(Deserialize, Debug, Clone)] From 97d50ef00555068fc21d6cde8a7a02b88640bb69 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Sun, 9 Mar 2025 11:55:49 -0400 Subject: [PATCH 5/5] docs: update docs about runner config --- docs/configuration.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 7996928..2aa8b21 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -148,7 +148,9 @@ See [templates documentation](./templates.md) for more information on the expect ## custom_target.\.runner.\ -`runner` is a table that defines how to run the test suites and parse the results. There can be multiple runners defined for one target (for example, testing multiple platforms) +`runner` is a table that defines how to run the test suites and parse the results. There can be multiple runners defined for one target (for example, testing multiple platforms). + +Each runner will inherit the fields of the previously-defined runner if they are not defined. ### Fields