use std::path::Path;
use std::process::ExitCode;
use fairlead_core::pattern::Pattern;
use serde_json::Value;
#[derive(clap::Args)]
pub struct InitArgs {
#[arg(long)]
dry_run: bool,
#[arg(long)]
force: bool,
#[arg(long, value_name = "NAME_OR_PATH")]
blueprint: Option<String>,
#[arg(long, value_name = "NAME")]
pack: Vec<String>,
#[arg(long, value_enum, default_value_t = InitProfile::Full, conflicts_with_all = ["blueprint", "pack"])]
profile: InitProfile,
}
#[derive(Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum InitProfile {
Full,
Guard,
}
struct Runner {
id: &'static str,
why: String,
matches: Vec<String>,
exclude: Vec<String>,
command: Vec<String>,
files: usize,
}
struct Check {
id: &'static str,
why: &'static str,
paths: Vec<&'static str>,
command: Vec<&'static str>,
}
struct Fmt {
why: String,
on: bool,
}
struct Found {
runners: Vec<Runner>,
checks: Vec<Check>,
fmt: Option<Fmt>,
notes: Vec<String>,
}
pub fn runners_now(root: &Path) -> Vec<&'static str> {
let files = fairlead_lang::tree::Tree::scan(&fairlead_lang::tree::plain(root)).files;
runners_in(root, &files)
}
pub fn runners_in(root: &Path, files: &[String]) -> Vec<&'static str> {
detect(root, files).runners.iter().map(|r| r.id).collect()
}
pub fn plain(root: &Path, files: &[String]) -> (String, bool) {
let found = detect(root, files);
if found.runners.is_empty() && found.checks.is_empty() {
return (crate::init_guard::render(root, files), true);
}
(render(&found), false)
}
pub fn run(args: InitArgs, cwd: &Path) -> ExitCode {
let root = crate::graph_cmd::repo_root(cwd);
let target = root.join("fairlead.toml");
if target.exists() && !args.force && !args.dry_run {
eprintln!(
"fairlead: {} already exists; `fairlead init --force` replaces it, `--dry-run` prints what init would write",
target.display()
);
return ExitCode::from(2);
}
if args.profile == InitProfile::Guard {
return crate::init_guard::run(&root, args.dry_run);
}
let files = fairlead_lang::tree::Tree::scan(&fairlead_lang::tree::plain(&root)).files;
let blueprint = match args
.blueprint
.as_deref()
.map(|b| crate::blueprint::load(b, cwd))
{
Some(Ok(bp)) => Some(bp),
Some(Err(e)) => {
eprintln!("fairlead: {e}");
return ExitCode::from(2);
}
None => None,
};
let found = detect(&root, &files);
if found.runners.is_empty() && found.checks.is_empty() {
if blueprint.is_some() || !args.pack.is_empty() {
eprintln!("fairlead: found no test runner init knows here, and a blueprint or a pack needs one; the book's Configuration page shows how to add a runner");
return ExitCode::FAILURE;
}
println!("fairlead: no test runner here yet, so this sets up the guard only. When there are tests, `fairlead doctor` says so and `fairlead init --dry-run` prints the runner to add");
for note in &found.notes {
println!(" note: {note}");
}
return crate::init_guard::run(&root, args.dry_run);
}
let mut text = render(&found);
if let Some(bp) = &blueprint {
text = with_blueprint(&text, bp);
}
if !args.pack.is_empty() {
text = with_packs(&text, &args.pack);
}
if args.dry_run {
print!("{text}");
for f in blueprint.iter().flat_map(|bp| &bp.files) {
println!("# would write {}", f.path);
}
return ExitCode::SUCCESS;
}
if let Err(lines) = crate::init_guard::land(&root, &text) {
for line in lines {
eprintln!("fairlead: {line}");
}
return ExitCode::from(2);
}
println!("fairlead: wrote {}", target.display());
if let Some(bp) = &blueprint {
if let Err(e) = write_blueprint(&root, bp, args.force) {
eprintln!("fairlead: {e}");
return ExitCode::from(2);
}
} else if let Some(bp) = crate::blueprint::suggest(&files) {
println!(
" this looks like {}: `fairlead init --blueprint {} --force` also adds {}",
bp.name, bp.name, bp.description
);
}
for (pack, why) in packs_for(&root, &files) {
if !extended(&text).iter().any(|p| p == pack) {
println!(
" {why}: `extends = [\"{pack}\"]` adds its pack (`fairlead init --pack {pack} --force`)"
);
}
}
summary(&found);
ExitCode::SUCCESS
}
fn with_blueprint(text: &str, bp: &crate::blueprint::Blueprint) -> String {
let lines: Vec<&str> = bp.config.trim().lines().collect();
let split = lines
.iter()
.position(|l| l.trim_start().starts_with('['))
.unwrap_or(lines.len());
let top = lines[..split].join("\n");
let tables = lines[split..].join("\n");
let at = text.find("\n[").map_or(text.len(), |i| i + 1);
let mut out = text[..at].to_string();
if !top.trim().is_empty() {
out.push_str(&format!(
"# From the {} blueprint.\n{}\n\n",
bp.name,
top.trim()
));
}
out.push_str(&text[at..]);
if !tables.trim().is_empty() {
out.push_str(&format!(
"\n# From the {} blueprint.\n{}\n",
bp.name,
tables.trim()
));
}
out
}
fn packs_for(root: &Path, files: &[String]) -> Vec<(&'static str, &'static str)> {
let has = |name: &str| files.iter().any(|f| f == name);
let read = |name: &str| std::fs::read_to_string(root.join(name)).unwrap_or_default();
let mut out = Vec::new();
if has("artisan") && read("composer.json").contains("\"laravel/framework\"") {
out.push(("laravel", "this is a Laravel app"));
}
let manifest: Value = serde_json::from_str(&read("package.json")).unwrap_or(Value::Null);
if ["dependencies", "devDependencies"]
.iter()
.any(|k| manifest[k].get("next").is_some())
{
out.push(("next", "this is a Next.js app"));
}
out
}
fn extended(text: &str) -> Vec<String> {
let Some(line) = text.lines().find(|l| l.starts_with("extends = [")) else {
return Vec::new();
};
line.split('"')
.skip(1)
.step_by(2)
.map(str::to_string)
.collect()
}
fn with_packs(text: &str, packs: &[String]) -> String {
let mut names = extended(text);
for p in packs {
if !names.contains(p) {
names.push(p.clone());
}
}
let line = format!(
"extends = [{}]",
names
.iter()
.map(|n| format!("\"{n}\""))
.collect::<Vec<_>>()
.join(", ")
);
if let Some(old) = text.lines().find(|l| l.starts_with("extends = [")) {
return text.replacen(old, &line, 1);
}
let at = text.find("\n[").map_or(text.len(), |i| i + 1);
format!(
"{}# Framework packs, from `fairlead init --pack`.\n{line}\n\n{}",
&text[..at],
&text[at..]
)
}
fn write_blueprint(
root: &Path,
bp: &crate::blueprint::Blueprint,
force: bool,
) -> Result<(), String> {
for f in &bp.files {
let path = root.join(&f.path);
if path.exists() && !force {
println!(
" kept {}: it's there already (--force replaces it)",
f.path
);
continue;
}
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir)
.map_err(|e| format!("could not create {}: {e}", dir.display()))?;
}
std::fs::write(&path, f.text.trim_start())
.map_err(|e| format!("could not write {}: {e}", path.display()))?;
println!(" wrote {}", f.path);
}
if bp.ci_workflow {
crate::ci_workflow::run(root, true, false)?;
}
Ok(())
}
fn summary(found: &Found) {
for r in &found.runners {
let plural = if r.files == 1 { "" } else { "s" };
println!(
" {:<10} {:>4} test file{plural} {}",
r.id,
r.files,
r.command.join(" ")
);
}
for c in &found.checks {
println!(" {:<10} check {}", c.id, c.command.join(" "));
}
match &found.fmt {
Some(f) if f.on => println!(" {:<10} check {}", "bun-fmt", FMT_COMMAND.join(" ")),
Some(_) => println!(
" {:<10} offered {} (commented out: uncomment it to gate formatting per change)",
"bun-fmt",
FMT_COMMAND.join(" ")
),
None => {}
}
for note in &found.notes {
println!(" note: {note}");
}
println!("next, in this order:");
println!(
" fairlead hooks install guard an agent's edits and check its work when it stops"
);
println!(" fairlead hooks install --git the same rules at commit, through lefthook");
println!(" fairlead agents sync tell the agent the change loop, in AGENTS.md");
println!(" fairlead ci workflow --write run the plan on pull requests");
println!(" fairlead done the gate: run it once and let it pass");
println!(" fairlead doctor what is still missing, with the command for each");
}
fn detect(root: &Path, files: &[String]) -> Found {
let mut found = Found {
runners: Vec::new(),
checks: Vec::new(),
fmt: None,
notes: Vec::new(),
};
let has = |name: &str| files.iter().any(|f| f == name);
if has("package.json") {
javascript(root, files, &mut found);
found.fmt = bun_fmt(root);
}
if has("go.mod") {
let matches = vec!["**/*_test.go".to_string()];
push(
&mut found,
files,
"go",
"go.mod",
matches,
args(&["go", "test", "{packages}"]),
);
} else if files.iter().any(|f| f.ends_with("/go.mod")) {
found.notes.push(
"a Go module below the root: add a runner per module with `invoke = \"per-module\"`"
.into(),
);
}
if [
"pyproject.toml",
"setup.py",
"setup.cfg",
"pytest.ini",
"tox.ini",
]
.iter()
.any(|f| has(f))
{
python(files, &mut found);
}
if has("composer.json") {
php(root, files, &mut found);
}
fallbacks(files, &mut found);
found
}
fn javascript(root: &Path, files: &[String], found: &mut Found) {
let manifest: Value = std::fs::read_to_string(root.join("package.json"))
.ok()
.and_then(|t| serde_json::from_str(&t).ok())
.unwrap_or(Value::Null);
let depends = |name: &str| {
["dependencies", "devDependencies"]
.iter()
.any(|k| manifest[k].get(name).is_some())
};
let resolved = test_scripts(&manifest);
let script = resolved.as_str();
let exec = exec_prefix(files);
let (bun_why, bun_command) = bun_runner(root);
let standard = vec!["**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs,mts,cts}".to_string()];
let with = |tool: &[&str]| {
let mut argv: Vec<String> = exec.iter().map(|s| s.to_string()).collect();
argv.extend(tool.iter().map(|s| s.to_string()));
argv
};
let (id, why, matches, command) = if depends("vitest") {
(
"vitest",
"vitest in package.json",
standard,
with(&["vitest", "run", "{files}"]),
)
} else if depends("jest") {
(
"jest",
"jest in package.json",
standard,
with(&["jest", "{files}"]),
)
} else if script.starts_with("bun test") {
("bun", bun_why.as_str(), standard, bun_command)
} else if script.contains("node --test") {
(
"node",
"`node --test` in scripts.test",
standard,
args(&["node", "--test", "{files}"]),
)
} else if depends("ava") {
let matches = match manifest["ava"]["files"].as_array() {
Some(globs) => globs
.iter()
.filter_map(|g| g.as_str())
.map(String::from)
.collect(),
None => [
"**/*.{test,spec}.{js,mjs,cjs,ts}",
"**/test-*.{js,mjs,cjs,ts}",
"test/**/*.{js,mjs,cjs,ts}",
"tests/**/*.{js,mjs,cjs,ts}",
"**/__tests__/**/*.{js,mjs,cjs,ts}",
]
.map(String::from)
.to_vec(),
};
found.notes.push(
"ava: copy any options or environment (such as NODE_OPTIONS) from scripts.test into the runner's command"
.into(),
);
(
"ava",
"ava in package.json",
matches,
with(&["ava", "{files}"]),
)
} else if depends("mocha") {
let spec_dir = vec!["test/**/*.{js,mjs,cjs,ts}".to_string()];
let matches = if count(files, &standard) > 0 {
standard
} else {
spec_dir
};
found.notes.push(
"mocha: copy any `--require` or reporter options from scripts.test into the runner's command"
.into(),
);
(
"mocha",
"mocha in package.json",
matches,
with(&["mocha", "{files}"]),
)
} else {
if !script.is_empty() {
found.notes.push(format!(
"the test scripts run `{script}`, which init doesn't recognise; add a [[tests.runners]] entry for it"
));
}
return;
};
let modules: &[&str] = match id {
"vitest" => &["vitest"],
"jest" => &["@jest/globals"],
"bun" => &["bun:test"],
"node" => &["node:test"],
_ => &[],
};
let mut matches = matches;
matches.extend(importing(root, files, &matches, modules));
let specs = match depends("@playwright/test") {
true => crate::init_playwright::specs(root, files, &matches),
false => Vec::new(),
};
let spec_globs = crate::init_playwright::globs(files, &matches, &specs);
push(found, files, id, why, matches, command);
if specs.is_empty() {
return;
}
match found.runners.last_mut().filter(|r| r.id == id) {
Some(unit) if unit.files > specs.len() => {
unit.files -= specs.len();
unit.exclude = spec_globs.clone();
}
Some(_) => {
found.runners.pop();
}
None => {}
}
found.runners.push(Runner {
id: "playwright",
why: "@playwright/test in package.json, on the specs that import it".into(),
matches: spec_globs,
exclude: Vec::new(),
command: with(&["playwright", "test", "{files}"]),
files: specs.len(),
});
found.notes.push(
"playwright: name the 1 to 3 specs that stand for the suite as `smoke` on its runner, and add a `[[needs]]` entry if the specs need a browser or a running app this machine may not have"
.into(),
);
}
fn bun_runner(root: &Path) -> (String, Vec<String>) {
match crate::bun::detect(root).filter(|(v, _)| v.at_least(crate::bun::PARALLEL)) {
Some((v, from)) => (
format!(
"`bun test` in scripts.test and Bun {v} in {from}; `--no-isolate`, as a process per file can be slower than serial on many small files"
),
args(&["bun", "test", "--parallel", "--no-isolate", "{files}"]),
),
None => (
"`bun test` in scripts.test".to_string(),
args(&["bun", "test", "{files}"]),
),
}
}
fn bun_fmt(root: &Path) -> Option<Fmt> {
let (v, from) = crate::bun::detect(root).filter(|(v, _)| v.at_least(crate::bun::PARALLEL))?;
let manifest: Value = std::fs::read_to_string(root.join("package.json"))
.ok()
.and_then(|t| serde_json::from_str(&t).ok())
.unwrap_or(Value::Null);
let on = manifest["scripts"].as_object().is_some_and(|scripts| {
scripts
.values()
.filter_map(Value::as_str)
.any(|s| s.contains("bun fmt"))
});
Some(Fmt {
why: format!("Bun {v} in {from}"),
on,
})
}
const FMT_PATHS: &[&str] = &["**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}"];
const FMT_COMMAND: &[&str] = &["bun", "fmt", "--no-script", "--check", "{files}"];
fn render_fmt(fmt: &Fmt) -> String {
let table = format!(
"[[checks]]\nid = \"bun-fmt\"\npaths = {}\ncommand = {}\n",
list(FMT_PATHS),
list(FMT_COMMAND)
);
if fmt.on {
return format!(
"\n# {}, and a script runs `bun fmt`: the files a change touches must be formatted.\n{table}",
fmt.why
);
}
let commented: String = table.lines().map(|l| format!("# {l}\n")).collect();
format!(
"\n# {} has `bun fmt --check`.\n\
# Uncomment this to gate formatting per change: only the files a change\n\
# touches must be formatted, so the formatter comes in without a one-shot\n\
# reformat. A plan that runs everything checks every file.\n{commented}",
fmt.why
)
}
fn importing(root: &Path, files: &[String], matches: &[String], modules: &[&str]) -> Vec<String> {
if modules.is_empty() {
return Vec::new();
}
let known: Vec<Pattern> = matches
.iter()
.filter_map(|g| Pattern::new(g).ok())
.collect();
let quoted: Vec<String> = modules
.iter()
.flat_map(|m| [format!("\"{m}\""), format!("'{m}'")])
.collect();
let mut names: std::collections::BTreeMap<&str, Vec<&String>> = Default::default();
for file in files.iter().filter(|f| !f.contains("node_modules/")) {
let name = file.rsplit('/').next().unwrap_or(file);
let script = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"]
.iter()
.any(|e| name.ends_with(e));
if !script || !(name.contains("test") || name.contains("spec")) {
continue;
}
if known.iter().any(|p| p.is_match(file)) {
continue;
}
let Ok(text) = std::fs::read_to_string(root.join(file)) else {
continue;
};
if quoted.iter().any(|q| text.contains(q.as_str())) {
names.entry(name).or_default().push(file);
}
}
names
.into_iter()
.flat_map(|(name, paths)| match paths.as_slice() {
[one] => vec![(*one).clone()],
_ => vec![format!("**/{name}")],
})
.collect()
}
fn test_scripts(manifest: &Value) -> String {
let scripts = &manifest["scripts"];
let test = scripts["test"].as_str().unwrap_or("");
let mut parts = Vec::new();
for step in test.split("&&").map(str::trim).filter(|s| !s.is_empty()) {
let words: Vec<&str> = step.split_whitespace().collect();
let name = match words.as_slice() {
["npm" | "pnpm" | "bun", "run", name, ..] => Some(*name),
["yarn", name, ..] if *name != "run" => Some(*name),
["yarn", "run", name, ..] => Some(*name),
_ => None,
};
match name.and_then(|n| scripts[n].as_str()) {
Some(inner) => parts.push(inner.to_string()),
None => parts.push(step.to_string()),
}
}
parts.join(" && ")
}
fn exec_prefix(files: &[String]) -> Vec<&'static str> {
let has = |name: &str| files.iter().any(|f| f == name);
if has("bun.lock") || has("bun.lockb") {
vec!["bun", "x"]
} else if has("pnpm-lock.yaml") {
vec!["pnpm", "exec"]
} else if has("yarn.lock") {
vec!["yarn"]
} else {
vec!["npx"]
}
}
fn python(files: &[String], found: &mut Found) {
let has = |name: &str| files.iter().any(|f| f == name);
let matches: Vec<String> = ["**/test_*.py", "**/*_test.py"]
.iter()
.map(|s| s.to_string())
.filter(|glob| count(files, std::slice::from_ref(glob)) > 0)
.collect();
let (why, command) = if has("uv.lock") {
("uv.lock", args(&["uv", "run", "pytest", "{files}"]))
} else if has("poetry.lock") {
("poetry.lock", args(&["poetry", "run", "pytest", "{files}"]))
} else {
(
"a Python project file",
args(&["python", "-m", "pytest", "{files}"]),
)
};
push(found, files, "pytest", why, matches, command);
if has("tests/runtests.py") {
found.notes.push(
"tests/runtests.py is this project's own test runner; if its suite doesn't run under pytest, change the pytest runner's command"
.into(),
);
}
}
fn php(root: &Path, files: &[String], found: &mut Found) {
let composer = std::fs::read_to_string(root.join("composer.json")).unwrap_or_default();
let has = |name: &str| files.iter().any(|f| f == name);
let (id, why, command) = if has("artisan") {
(
"artisan",
"Laravel's artisan",
args(&["php", "artisan", "test", "{files}"]),
)
} else if composer.contains("\"pestphp/pest\"") {
(
"pest",
"pestphp/pest in composer.json",
args(&["vendor/bin/pest", "{files}"]),
)
} else if has("phpunit.xml")
|| has("phpunit.xml.dist")
|| composer.contains("\"phpunit/phpunit\"")
{
(
"phpunit",
"PHPUnit's config",
args(&["vendor/bin/phpunit", "{files}"]),
)
} else {
return;
};
let matches =
phpunit_dirs(root, files).unwrap_or_else(|| vec!["tests/**/*Test.php".to_string()]);
push(found, files, id, why, matches, command);
}
fn phpunit_dirs(root: &Path, files: &[String]) -> Option<Vec<String>> {
let name = ["phpunit.xml", "phpunit.xml.dist"]
.into_iter()
.find(|n| files.iter().any(|f| f == n))?;
let text = std::fs::read_to_string(root.join(name)).ok()?;
let directory = regex::Regex::new(r#"<directory([^>]*)>\s*([^<]+?)\s*</directory>"#).ok()?;
let suffix = regex::Regex::new(r#"suffix\s*=\s*"([^"]+)""#).ok()?;
let mut out: Vec<String> = directory
.captures_iter(&text)
.map(|c| {
let dir = c[2].trim_start_matches("./").trim_end_matches('/');
let end = suffix
.captures(&c[1])
.map_or("Test.php".to_string(), |s| s[1].to_string());
format!("{dir}/**/*{end}")
})
.collect();
out.sort();
out.dedup();
(!out.is_empty()).then_some(out)
}
fn fallbacks(files: &[String], found: &mut Found) {
let has = |name: &str| files.iter().any(|f| f == name);
if has("Cargo.toml") {
found.checks.push(Check {
id: "cargo-test",
why: "Rust isn't in the import graph yet",
paths: vec!["**/*.rs", "**/Cargo.toml", "Cargo.lock"],
command: vec!["cargo", "test"],
});
}
if has("gradlew") {
found.checks.push(Check {
id: "gradle-test",
why: "Java and Kotlin aren't in the import graph yet",
paths: vec!["**/*.java", "**/*.kt", "**/*.gradle", "**/*.gradle.kts"],
command: vec!["./gradlew", "test"],
});
} else if has("pom.xml") {
found.checks.push(Check {
id: "maven-test",
why: "Java isn't in the import graph yet",
paths: vec!["**/*.java", "**/pom.xml"],
command: vec!["mvn", "-q", "test"],
});
}
if files
.iter()
.any(|f| f.ends_with(".sln") || f.ends_with(".csproj"))
{
found.checks.push(Check {
id: "dotnet-test",
why: ".NET isn't in the import graph yet",
paths: vec!["**/*.cs", "**/*.csproj", "**/*.sln"],
command: vec!["dotnet", "test"],
});
}
if has("Gemfile")
&& !files.iter().any(|f| f.starts_with("spec/"))
&& files
.iter()
.any(|f| f.starts_with("test/") && f.ends_with("_test.rb"))
{
found.checks.push(Check {
id: "rake-test",
why: "Ruby isn't in the import graph yet",
paths: vec!["**/*.rb", "Gemfile", "Gemfile.lock"],
command: vec!["bundle", "exec", "rake", "test"],
});
}
if has("Gemfile") && files.iter().any(|f| f.starts_with("spec/")) {
found.checks.push(Check {
id: "rspec",
why: "Ruby isn't in the import graph yet",
paths: vec!["**/*.rb", "Gemfile", "Gemfile.lock"],
command: vec!["bundle", "exec", "rspec"],
});
}
}
fn push(
found: &mut Found,
files: &[String],
id: &'static str,
why: &str,
matches: Vec<String>,
command: Vec<String>,
) {
let n = count(files, &matches);
if n == 0 {
found.notes.push(format!(
"{id}: {why}, but no test file matches {}; add a runner once there are tests",
if matches.is_empty() {
"its patterns".into()
} else {
matches.join(", ")
}
));
return;
}
found.runners.push(Runner {
id,
why: why.to_string(),
matches,
exclude: Vec::new(),
command,
files: n,
});
}
fn count(files: &[String], globs: &[String]) -> usize {
let patterns: Vec<Pattern> = globs.iter().filter_map(|g| Pattern::new(g).ok()).collect();
files
.iter()
.filter(|f| !f.contains("node_modules/") && !f.starts_with("vendor/"))
.filter(|f| patterns.iter().any(|p| p.is_match(f)))
.count()
}
fn args(parts: &[&str]) -> Vec<String> {
parts.iter().map(|s| s.to_string()).collect()
}
fn list<S: AsRef<str>>(items: &[S]) -> String {
let quoted: Vec<String> = items
.iter()
.map(|s| {
format!(
"\"{}\"",
s.as_ref().replace('\\', "\\\\").replace('"', "\\\"")
)
})
.collect();
format!("[{}]", quoted.join(", "))
}
fn render(found: &Found) -> String {
let version = env!("CARGO_PKG_VERSION");
let minor = version.rsplit_once('.').map_or(version, |(m, _)| m);
let mut out = format!(
"# Written by `fairlead init` from what this repository shows. Edit freely:\n\
# `fairlead config check` validates it, and the book's Configuration page\n\
# lists every key.\n\
fairlead = \"{minor}\"\n"
);
let mut all: Vec<&String> = found.runners.iter().flat_map(|r| &r.matches).collect();
all.sort();
all.dedup();
out.push_str(&format!(
"\n[tests]\nmatch = {{ replace = {} }}\n",
list(&all)
));
for r in &found.runners {
let exclude = match r.exclude.is_empty() {
true => String::new(),
false => format!("exclude = {}\n", list(&r.exclude)),
};
out.push_str(&format!(
"\n# {}: {} test files, from {}.\n[[tests.runners]]\nid = \"{}\"\nmatch = {}\n{exclude}command = {}\n",
r.id,
r.files,
r.why,
r.id,
list(&r.matches),
list(&r.command)
));
}
for c in &found.checks {
out.push_str(&format!(
"\n# {}, so any change to it runs the whole suite.\n[[checks]]\nid = \"{}\"\npaths = {}\ncommand = {}\n",
c.why,
c.id,
list(&c.paths),
list(&c.command)
));
}
if let Some(f) = &found.fmt {
out.push_str(&render_fmt(f));
}
out
}