use clap::Args;
use homeboy::engine::execution_context::{self, ResolveOptions};
use homeboy::engine::run_dir::RunDir;
use homeboy::extension::test as extension_test;
use homeboy::extension::test::{
detect_test_drift, report, run_self_check_test_workflow, TestCommandOutput, TestRunWorkflowArgs,
};
use homeboy::extension::ExtensionCapability;
use super::utils::args::{
BaselineArgs, ExtensionOverrideArgs, HiddenJsonArgs, PositionalComponentArgs, SettingArgs,
};
use super::{CmdResult, GlobalArgs};
#[derive(Args)]
pub struct TestArgs {
#[command(flatten)]
pub comp: PositionalComponentArgs,
#[command(flatten)]
pub extension_override: ExtensionOverrideArgs,
#[arg(long)]
pub skip_lint: bool,
#[arg(long)]
pub coverage: bool,
#[arg(long, value_name = "PERCENT")]
pub coverage_min: Option<f64>,
#[command(flatten)]
pub baseline_args: BaselineArgs,
#[arg(long)]
pub analyze: bool,
#[arg(long)]
pub drift: bool,
#[arg(long)]
pub write: bool,
#[arg(long, value_name = "REF", default_value = "HEAD~10")]
pub since: String,
#[arg(long, value_name = "REF")]
pub changed_since: Option<String>,
#[command(flatten)]
pub setting_args: SettingArgs,
#[arg(last = true)]
pub args: Vec<String>,
#[command(flatten)]
pub _json: HiddenJsonArgs,
#[arg(long)]
pub json_summary: bool,
}
fn filter_homeboy_flags(args: &[String]) -> Vec<String> {
const HOMEBOY_FLAGS: &[&str] = &[
"--analyze",
"--drift",
"--write",
"--json-summary",
"--baseline",
"--ignore-baseline",
"--ratchet",
"--skip-lint",
"--coverage",
"--json",
];
const HOMEBOY_VALUE_FLAGS: &[&str] = &[
"--coverage-min",
"--since",
"--changed-since",
"--setting",
"--path",
"--extension",
];
let mut filtered = Vec::new();
let mut skip_next = false;
for arg in args {
if skip_next {
skip_next = false;
continue;
}
if HOMEBOY_FLAGS.contains(&arg.as_str()) {
continue;
}
let is_value_flag = HOMEBOY_VALUE_FLAGS.iter().any(|f| {
if arg.starts_with(&format!("{}=", f)) {
return true; }
if arg == *f {
skip_next = true; return true;
}
false
});
if is_value_flag {
continue;
}
filtered.push(arg.clone());
}
filtered
}
pub fn run(args: TestArgs, _global: &GlobalArgs) -> CmdResult<TestCommandOutput> {
let source_ctx = execution_context::resolve(&ResolveOptions {
component_id: args.comp.component.clone(),
path_override: args.comp.path.clone(),
capability: None,
settings_overrides: args.setting_args.setting.clone(),
settings_json_overrides: args.setting_args.setting_json.clone(),
extension_overrides: args.extension_override.extensions.clone(),
})?;
if args.extension_override.extensions.is_empty()
&& !args.drift
&& source_ctx
.component
.has_self_check(ExtensionCapability::Test)
{
let workflow = run_self_check_test_workflow(
&source_ctx.component,
&source_ctx.source_path,
source_ctx.component_id.clone(),
args.json_summary,
)?;
return Ok(report::from_main_workflow(workflow));
}
let ctx = execution_context::resolve(&ResolveOptions {
component_id: args.comp.component.clone(),
path_override: args.comp.path.clone(),
capability: Some(ExtensionCapability::Test),
settings_overrides: args.setting_args.setting.clone(),
settings_json_overrides: args.setting_args.setting_json.clone(),
extension_overrides: args.extension_override.extensions.clone(),
})?;
let effective_id = ctx.component_id.clone();
if args.drift {
let result = detect_test_drift(&effective_id, &ctx.component, &args.since)?;
return Ok(report::from_drift_workflow(result));
}
let run_dir = RunDir::create()?;
let passthrough_args = filter_homeboy_flags(&args.args);
let workflow = extension_test::run_main_test_workflow(
&ctx.component,
&ctx.source_path,
TestRunWorkflowArgs {
component_label: effective_id.clone(),
component_id: ctx.component_id.clone(),
path_override: args.comp.path.clone(),
settings: ctx
.settings
.iter()
.map(|(k, v)| {
(
k.clone(),
match v {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
},
)
})
.collect(),
skip_lint: args.skip_lint,
coverage: args.coverage,
coverage_min: args.coverage_min,
analyze: args.analyze,
baseline_flags: homeboy::engine::baseline::BaselineFlags {
baseline: args.baseline_args.baseline,
ignore_baseline: args.baseline_args.ignore_baseline,
ratchet: args.baseline_args.ratchet,
},
changed_since: args.changed_since.clone(),
json_summary: args.json_summary,
passthrough_args: passthrough_args.clone(),
},
&run_dir,
)?;
Ok(report::from_main_workflow(workflow))
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
use homeboy::component::Component;
use homeboy::refactor::plan::{build_test_refactor_request, TestSourceOptions};
use std::path::PathBuf;
#[derive(Parser)]
struct TestCli {
#[command(flatten)]
test: TestArgs,
}
#[test]
fn parses_one_shot_extension_override() {
let cli = TestCli::try_parse_from([
"test",
"--path",
"/tmp/repo",
"--extension",
"nodejs",
"--changed-since",
"origin/main",
])
.expect("test should parse --extension override");
assert_eq!(cli.test.extension_override.extensions, vec!["nodejs"]);
assert_eq!(cli.test.changed_since.as_deref(), Some("origin/main"));
}
#[test]
fn filter_strips_boolean_flags() {
let args = vec!["--analyze".to_string(), "--filter=SomeTest".to_string()];
let result = filter_homeboy_flags(&args);
assert_eq!(result, vec!["--filter=SomeTest"]);
}
#[test]
fn filter_strips_multiple_boolean_flags() {
let args = vec![
"--analyze".to_string(),
"--drift".to_string(),
"--baseline".to_string(),
"--ignore-baseline".to_string(),
"--ratchet".to_string(),
"--skip-lint".to_string(),
"--coverage".to_string(),
"--write".to_string(),
"--json".to_string(),
];
let result = filter_homeboy_flags(&args);
assert!(result.is_empty());
}
#[test]
fn filter_strips_value_flags_space_separated() {
let args = vec![
"--since".to_string(),
"v0.36.0".to_string(),
"--filter=SomeTest".to_string(),
];
let result = filter_homeboy_flags(&args);
assert_eq!(result, vec!["--filter=SomeTest"]);
let args = vec![
"--changed-since".to_string(),
"origin/main".to_string(),
"--filter=SomeTest".to_string(),
];
let result = filter_homeboy_flags(&args);
assert_eq!(result, vec!["--filter=SomeTest"]);
let args = vec![
"--extension".to_string(),
"nodejs".to_string(),
"--filter=SomeTest".to_string(),
];
let result = filter_homeboy_flags(&args);
assert_eq!(result, vec!["--filter=SomeTest"]);
}
#[test]
fn filter_strips_value_flags_equals_form() {
let args = vec![
"--since=v0.36.0".to_string(),
"--filter=SomeTest".to_string(),
];
let result = filter_homeboy_flags(&args);
assert_eq!(result, vec!["--filter=SomeTest"]);
}
#[test]
fn filter_strips_coverage_min() {
let args = vec![
"--coverage-min".to_string(),
"80".to_string(),
"--filter=SomeTest".to_string(),
];
let result = filter_homeboy_flags(&args);
assert_eq!(result, vec!["--filter=SomeTest"]);
}
#[test]
fn filter_strips_setting() {
let args = vec![
"--setting".to_string(),
"database_type=mysql".to_string(),
"--filter=SomeTest".to_string(),
];
let result = filter_homeboy_flags(&args);
assert_eq!(result, vec!["--filter=SomeTest"]);
}
#[test]
fn filter_preserves_unknown_flags() {
let args = vec![
"--filter=SomeTest".to_string(),
"--group".to_string(),
"ajax".to_string(),
"--verbose".to_string(),
];
let result = filter_homeboy_flags(&args);
assert_eq!(args, result);
}
#[test]
fn filter_handles_empty() {
let result = filter_homeboy_flags(&[]);
assert!(result.is_empty());
}
#[test]
fn filter_handles_mixed() {
let args = vec![
"--analyze".to_string(),
"--skip-lint".to_string(),
"--since".to_string(),
"v0.35.0".to_string(),
"--filter=FlowAbilities".to_string(),
"--coverage-min=80".to_string(),
"--verbose".to_string(),
];
let result = filter_homeboy_flags(&args);
assert_eq!(result, vec!["--filter=FlowAbilities", "--verbose"]);
}
#[test]
fn filter_strips_path_flag() {
let args = vec![
"--path".to_string(),
"/tmp/checkout".to_string(),
"--filter=SomeTest".to_string(),
];
let result = filter_homeboy_flags(&args);
assert_eq!(result, vec!["--filter=SomeTest"]);
}
#[test]
fn filter_strips_json_summary_flag() {
let args = vec![
"--json-summary".to_string(),
"--filter=SomeTest".to_string(),
];
let result = filter_homeboy_flags(&args);
assert_eq!(result, vec!["--filter=SomeTest"]);
}
#[test]
fn test_fix_builds_canonical_refactor_request() {
let component = Component::new(
"demo".to_string(),
"/tmp/demo".to_string(),
String::new(),
None,
);
let request = build_test_refactor_request(
component.clone(),
PathBuf::from("/tmp/demo"),
vec![("runner".to_string(), "ci".to_string())],
TestSourceOptions {
selected_files: Some(vec!["tests/demo_test.rs".to_string()]),
skip_lint: true,
script_args: vec!["--filter=DemoTest".to_string()],
},
true,
);
assert_eq!(request.component.id, component.id);
assert_eq!(request.sources, vec!["test".to_string()]);
assert!(request.write);
assert_eq!(request.settings.len(), 1);
assert!(request.lint.selected_files.is_none());
assert_eq!(request.test.selected_files.as_ref().unwrap().len(), 1);
assert!(request.test.skip_lint);
}
}