refactor(dev): dedupe shared helpers, remove check-boundary

Consolidate workspace_root, PlannedCommand, shell_arg, markdown_cell, and
replace_generated_region into commands/mod.rs; unify fabro_dev/output_text/
write_file/read_file into tests/it/main.rs. Drop the abandoned check-boundary
subcommand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-04-24 18:10:31 -04:00
parent 5e28ec0f82
commit a4ee62a8a4
No known key found for this signature in database
16 changed files with 181 additions and 751 deletions

View file

@ -1,194 +0,0 @@
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use clap::Args;
use regex::Regex;
use walkdir::WalkDir;
const FABRO_CLI_SRC: &str = "lib/crates/fabro-cli/src";
const EXEMPTION_MARKER: &str = "boundary-exempt(pr-api): remove with follow-up #1";
const SERVER_SYMBOL_ALLOWLIST: &[&str] = &[
"lib/crates/fabro-cli/src/local_server.rs",
"lib/crates/fabro-cli/src/commands/install.rs",
"lib/crates/fabro-cli/src/commands/run/runner.rs",
];
const STORAGE_ALLOWLIST: &[&str] = &[
"lib/crates/fabro-cli/src/command_context.rs",
"lib/crates/fabro-cli/src/commands/install.rs",
"lib/crates/fabro-cli/src/commands/uninstall.rs",
"lib/crates/fabro-cli/src/commands/run/runner.rs",
];
const DEPRECATED_HELPER_ALLOWLIST: &[&str] = &["lib/crates/fabro-cli/src/user_config.rs"];
const TEMPORARY_EXEMPTIONS: &[&str] = &[
"lib/crates/fabro-cli/src/commands/pr/mod.rs",
"lib/crates/fabro-cli/src/commands/pr/create.rs",
];
#[derive(Debug, Args)]
pub(crate) struct CheckBoundaryArgs {
/// Workspace root to check.
#[arg(long, value_name = "ROOT", default_value = ".")]
root: PathBuf,
}
struct BoundaryRule {
name: &'static str,
pattern: Regex,
allowlist: &'static [&'static str],
}
struct SourceFile {
relative: String,
contents: String,
}
#[expect(
clippy::print_stdout,
clippy::print_stderr,
reason = "dev check command reports pass/fail diagnostics directly"
)]
pub(crate) fn check_boundary(args: CheckBoundaryArgs) -> Result<()> {
let failures = BoundaryChecker::new(args.root).check()?;
if failures.is_empty() {
println!("CLI/server boundary checks passed.");
return Ok(());
}
for failure in failures {
eprintln!("{failure}");
}
bail!("CLI/server boundary checks failed")
}
struct BoundaryChecker {
root: PathBuf,
}
impl BoundaryChecker {
fn new(root: PathBuf) -> Self {
Self { root }
}
fn check(&self) -> Result<Vec<String>> {
let rules = boundary_rules();
let mut failures = Vec::new();
for file in self.source_files()? {
let has_marker = marker_lines(&file.contents).next().is_some();
let has_valid_exemption =
has_marker && TEMPORARY_EXEMPTIONS.contains(&file.relative.as_str());
for line_number in marker_lines(&file.contents) {
if !TEMPORARY_EXEMPTIONS.contains(&file.relative.as_str()) {
failures.push(format!(
"boundary check failed: unexpected temporary exemption marker in {}:{}",
file.relative, line_number
));
}
}
for rule in &rules {
if rule.allowlist.contains(&file.relative.as_str()) || has_valid_exemption {
continue;
}
for (line_number, line) in file.contents.lines().enumerate() {
if rule.pattern.is_match(line) {
failures.push(format!(
"boundary check failed: {} used outside allowlist: {}:{}",
rule.name,
file.relative,
line_number + 1
));
}
}
}
}
Ok(failures)
}
#[expect(
clippy::disallowed_methods,
reason = "dev boundary checker synchronously scans source files outside Tokio paths"
)]
fn source_files(&self) -> Result<Vec<SourceFile>> {
let source_root = self.root.join(FABRO_CLI_SRC);
if !source_root.exists() {
return Ok(Vec::new());
}
let mut files = Vec::new();
for entry in WalkDir::new(&source_root) {
let entry = entry.context("walking fabro-cli source tree")?;
let path = entry.path();
if !entry.file_type().is_file()
|| path.extension().and_then(|ext| ext.to_str()) != Some("rs")
{
continue;
}
let contents = fs::read_to_string(path)
.with_context(|| format!("reading source file {}", path.display()))?;
files.push(SourceFile {
relative: relative_path(&self.root, path)?,
contents,
});
}
files.sort_by(|left, right| left.relative.cmp(&right.relative));
Ok(files)
}
}
fn boundary_rules() -> [BoundaryRule; 3] {
[
BoundaryRule {
name: "gated server symbol",
pattern: Regex::new(
r"fabro_config::resolve_server_from_file|fabro_config::resolve_server\b|fabro_config::ServerSettings::from_layer\b|fabro_config::ServerSettings::resolve\b|ServerSettings::from_layer\b|ServerSettings::resolve\b",
)
.expect("server symbol regex should compile"),
allowlist: SERVER_SYMBOL_ALLOWLIST,
},
BoundaryRule {
name: "Storage::new",
pattern: Regex::new(r"Storage::new").expect("storage regex should compile"),
allowlist: STORAGE_ALLOWLIST,
},
BoundaryRule {
name: "deprecated user_config::storage_dir",
pattern: Regex::new(r"user_config::storage_dir")
.expect("deprecated helper regex should compile"),
allowlist: DEPRECATED_HELPER_ALLOWLIST,
},
]
}
fn marker_lines(contents: &str) -> impl Iterator<Item = usize> + '_ {
contents.lines().enumerate().filter_map(|(index, line)| {
if line.contains(EXEMPTION_MARKER) {
Some(index + 1)
} else {
None
}
})
}
fn relative_path(root: &Path, path: &Path) -> Result<String> {
let relative = path
.strip_prefix(root)
.with_context(|| format!("{} is not under {}", path.display(), root.display()))?;
Ok(relative
.components()
.map(|component| component.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("/"))
}

View file

@ -5,6 +5,8 @@ use anyhow::{Context, Result, bail};
use clap::Args;
use walkdir::WalkDir;
use super::workspace_root;
const DEFAULT_ASSET_BUDGET_BYTES: u64 = 15 * 1024 * 1024;
const DEFAULT_PAYLOAD_BUDGET_BYTES: u64 = 5 * 1024 * 1024;
@ -127,11 +129,3 @@ fn ensure_gzip_success(file: &Path, output: &Output) -> Result<u64> {
Ok(output.stdout.len() as u64)
}
fn workspace_root() -> PathBuf {
let mut root = Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf();
root.pop();
root.pop();
root.pop();
root
}

View file

@ -1,10 +1,11 @@
use std::fmt;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::path::PathBuf;
use anyhow::{Context, Result, bail};
use clap::{Args, ValueEnum};
use super::{PlannedCommand, command, shell_arg, workspace_root};
const ZIG_VERSION: &str = "0.13.0";
#[derive(Debug, Args)]
@ -199,13 +200,8 @@ impl DockerBuildPlan {
.arg(".")
}
#[expect(
clippy::disallowed_methods,
reason = "dev docker-build intentionally runs synchronous Docker subprocesses"
)]
fn run_command(&self, planned: &PlannedCommand) -> Result<()> {
let status = Command::new(&planned.program)
.args(&planned.args)
let status = command(planned)
.current_dir(&self.workspace_root)
.status()
.with_context(|| format!("running {}", planned.to_shell_line()))?;
@ -228,32 +224,6 @@ impl DockerBuildPlan {
}
}
struct PlannedCommand {
program: String,
args: Vec<String>,
}
impl PlannedCommand {
fn new(program: impl Into<String>) -> Self {
Self {
program: program.into(),
args: Vec::new(),
}
}
fn arg(mut self, arg: impl Into<String>) -> Self {
self.args.push(arg.into());
self
}
fn to_shell_line(&self) -> String {
std::iter::once(shell_arg(&self.program))
.chain(self.args.iter().map(shell_arg))
.collect::<Vec<_>>()
.join(" ")
}
}
fn build_script(target: &str, zig_arch: &str) -> String {
format!(
"set -e; \
@ -269,23 +239,3 @@ fn build_script(target: &str, zig_arch: &str) -> String {
cargo zigbuild --release -p fabro-cli --target {target}"
)
}
fn shell_arg(arg: impl AsRef<str>) -> String {
let arg = arg.as_ref();
if arg
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || "_-./:=@".contains(ch))
{
return arg.to_string();
}
format!("'{}'", arg.replace('\'', "'\\''"))
}
fn workspace_root() -> PathBuf {
let mut root = Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf();
root.pop();
root.pop();
root.pop();
root
}

View file

@ -1,9 +1,11 @@
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use anyhow::{Context, Result, bail};
use clap::{Arg, ArgAction, Command};
use super::{markdown_cell, replace_generated_region, workspace_root};
const CLI_REFERENCE_PATH: &str = "docs/reference/cli.mdx";
const FENCE_START: &str = "<!-- generated:cli -->";
const FENCE_END: &str = "<!-- /generated:cli -->";
@ -29,7 +31,13 @@ pub(crate) fn generate_cli_reference(args: GenerateCliReferenceArgs) -> Result<(
let current =
std::fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))?;
let generated = render_cli_reference(fabro_cli::command_for_reference());
let updated = replace_generated_region(&current, &generated)?;
let updated = replace_generated_region(
&current,
&generated,
CLI_REFERENCE_PATH,
FENCE_START,
FENCE_END,
)?;
if args.check {
if current != updated {
@ -254,53 +262,3 @@ fn os_str_to_markdown_code(value: &OsStr) -> Option<String> {
let value = value.to_str()?;
(!value.is_empty()).then(|| format!("`{}`", markdown_cell(value)))
}
fn markdown_cell(value: &str) -> String {
value
.replace('|', "\\|")
.replace('\n', "<br />")
.trim()
.to_string()
}
fn replace_generated_region(current: &str, generated: &str) -> Result<String> {
let start = current
.find(FENCE_START)
.with_context(|| format!("{CLI_REFERENCE_PATH} is missing {FENCE_START}"))?;
let content_start = start + FENCE_START.len();
let relative_end = current[content_start..]
.find(FENCE_END)
.with_context(|| format!("{CLI_REFERENCE_PATH} is missing {FENCE_END}"))?;
let end = content_start + relative_end;
let before = &current[..content_start];
let after = &current[end..];
Ok(format!("{before}\n{generated}\n{after}"))
}
fn workspace_root() -> PathBuf {
let mut root = Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf();
root.pop();
root.pop();
root.pop();
root
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn replace_generated_region_preserves_manual_content() {
let updated = replace_generated_region(
"before\n<!-- generated:cli -->\nstale\n<!-- /generated:cli -->\nafter\n",
"fresh",
)
.expect("generated region should be replaced");
assert_eq!(
updated,
"before\n<!-- generated:cli -->\nfresh\n<!-- /generated:cli -->\nafter\n"
);
}
}

View file

@ -1,9 +1,11 @@
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use anyhow::{Context, Result, bail};
use fabro_options_metadata::{OptionField, OptionSet, Visit};
use super::{markdown_cell, replace_generated_region, workspace_root};
const OPTIONS_REFERENCE_PATH: &str = "docs/reference/user-configuration.mdx";
const FENCE_START: &str = "<!-- generated:options -->";
const FENCE_END: &str = "<!-- /generated:options -->";
@ -30,7 +32,13 @@ pub(crate) fn generate_options_reference(args: GenerateOptionsReferenceArgs) ->
let current =
std::fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))?;
let generated = render_options_reference();
let updated = replace_generated_region(&current, &generated)?;
let updated = replace_generated_region(
&current,
&generated,
OPTIONS_REFERENCE_PATH,
FENCE_START,
FENCE_END,
)?;
if args.check {
if current != updated {
@ -276,53 +284,3 @@ See [MCP](/agents/mcp) for transport-specific examples.
fn normalize_doc(doc: &str) -> String {
doc.trim().trim_end_matches('.').to_string()
}
fn markdown_cell(value: &str) -> String {
value
.replace('|', "\\|")
.replace('\n', "<br />")
.trim()
.to_string()
}
fn replace_generated_region(current: &str, generated: &str) -> Result<String> {
let start = current
.find(FENCE_START)
.with_context(|| format!("{OPTIONS_REFERENCE_PATH} is missing {FENCE_START}"))?;
let content_start = start + FENCE_START.len();
let relative_end = current[content_start..]
.find(FENCE_END)
.with_context(|| format!("{OPTIONS_REFERENCE_PATH} is missing {FENCE_END}"))?;
let end = content_start + relative_end;
let before = &current[..content_start];
let after = &current[end..];
Ok(format!("{before}\n{generated}\n{after}"))
}
fn workspace_root() -> PathBuf {
let mut root = Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf();
root.pop();
root.pop();
root.pop();
root
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn replace_generated_region_preserves_manual_content() {
let updated = replace_generated_region(
"before\n<!-- generated:options -->\nstale\n<!-- /generated:options -->\nafter\n",
"fresh",
)
.expect("generated region should be replaced");
assert_eq!(
updated,
"before\n<!-- generated:options -->\nfresh\n<!-- /generated:options -->\nafter\n"
);
}
}

View file

@ -1,4 +1,3 @@
mod check_boundary;
mod check_spa_budgets;
mod docker_build;
mod generate_cli_reference;
@ -6,7 +5,10 @@ mod generate_options_reference;
mod refresh_spa;
mod release;
pub(crate) use check_boundary::{CheckBoundaryArgs, check_boundary};
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Context, Result};
pub(crate) use check_spa_budgets::{CheckSpaBudgetsArgs, check_spa_budgets};
pub(crate) use docker_build::{DockerBuildArgs, docker_build};
pub(crate) use generate_cli_reference::{GenerateCliReferenceArgs, generate_cli_reference};
@ -15,3 +17,120 @@ pub(crate) use generate_options_reference::{
};
pub(crate) use refresh_spa::{RefreshSpaArgs, refresh_spa};
pub(crate) use release::{ReleaseArgs, release};
pub(crate) fn workspace_root() -> PathBuf {
let mut root = Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf();
root.pop();
root.pop();
root.pop();
root
}
pub(crate) fn markdown_cell(value: &str) -> String {
value
.replace('|', "\\|")
.replace('\n', "<br />")
.trim()
.to_string()
}
pub(crate) fn replace_generated_region(
current: &str,
generated: &str,
doc_path: &str,
fence_start: &str,
fence_end: &str,
) -> Result<String> {
let start = current
.find(fence_start)
.with_context(|| format!("{doc_path} is missing {fence_start}"))?;
let content_start = start + fence_start.len();
let relative_end = current[content_start..]
.find(fence_end)
.with_context(|| format!("{doc_path} is missing {fence_end}"))?;
let end = content_start + relative_end;
let before = &current[..content_start];
let after = &current[end..];
Ok(format!("{before}\n{generated}\n{after}"))
}
pub(crate) struct PlannedCommand {
pub(crate) program: String,
pub(crate) args: Vec<String>,
pub(crate) unset_env: Vec<String>,
pub(crate) env: Vec<(String, String)>,
}
impl PlannedCommand {
pub(crate) fn new(program: impl Into<String>) -> Self {
Self {
program: program.into(),
args: Vec::new(),
unset_env: Vec::new(),
env: Vec::new(),
}
}
pub(crate) fn arg(mut self, arg: impl Into<String>) -> Self {
self.args.push(arg.into());
self
}
pub(crate) fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env.push((key.into(), value.into()));
self
}
pub(crate) fn env_remove(mut self, key: impl Into<String>) -> Self {
self.unset_env.push(key.into());
self
}
pub(crate) fn to_shell_line(&self) -> String {
let mut parts = Vec::new();
if !self.unset_env.is_empty() {
parts.push("unset".to_string());
parts.extend(self.unset_env.iter().map(shell_arg));
parts.push("&&".to_string());
}
parts.extend(
self.env
.iter()
.map(|(key, value)| format!("{}={}", shell_arg(key), shell_arg(value))),
);
parts.push(shell_arg(&self.program));
parts.extend(self.args.iter().map(shell_arg));
parts.join(" ")
}
}
#[expect(
clippy::disallowed_methods,
reason = "fabro-dev intentionally builds synchronous subprocess commands"
)]
pub(crate) fn command(planned: &PlannedCommand) -> Command {
let mut command = Command::new(&planned.program);
command.args(&planned.args);
for key in &planned.unset_env {
command.env_remove(key);
}
for (key, value) in &planned.env {
command.env(key, value);
}
command
}
pub(crate) fn shell_arg(arg: impl AsRef<str>) -> String {
let arg = arg.as_ref();
if arg
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || "_-./:=@".contains(ch))
{
return arg.to_string();
}
format!("'{}'", arg.replace('\'', "'\\''"))
}

View file

@ -5,6 +5,8 @@ use anyhow::{Context, Result, bail};
use clap::Args;
use walkdir::WalkDir;
use super::workspace_root;
#[derive(Debug, Args)]
pub(crate) struct RefreshSpaArgs {
/// Repository root containing apps/fabro-web and lib/crates/fabro-spa.
@ -101,11 +103,3 @@ fn mirror_dist(dist_dir: &Path, asset_dir: &Path) -> Result<()> {
Ok(())
}
fn workspace_root() -> PathBuf {
let mut root = Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf();
root.pop();
root.pop();
root.pop();
root
}

View file

@ -1,11 +1,13 @@
use std::fmt;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::process::Output;
use anyhow::{Context, Result, bail};
use chrono::{Local, NaiveDate};
use clap::{Args, ValueEnum};
use super::{PlannedCommand, command, workspace_root};
const RELEASE_EPOCH: &str = "2026-01-01";
const RELEASE_TEST_SEGMENT_WRITE_KEY: &str = "fake-for-local-smoke";
@ -353,91 +355,3 @@ fn update_version(cargo_toml: &Path, current_version: &str, new_version: &str) -
std::fs::write(cargo_toml, contents.replacen(&needle, &replacement, 1))
.with_context(|| format!("writing {}", cargo_toml.display()))
}
#[expect(
clippy::disallowed_methods,
reason = "dev release builds synchronous subprocess commands"
)]
fn command(planned: &PlannedCommand) -> Command {
let mut command = Command::new(&planned.program);
command.args(&planned.args);
for key in &planned.unset_env {
command.env_remove(key);
}
for (key, value) in &planned.env {
command.env(key, value);
}
command
}
struct PlannedCommand {
program: String,
args: Vec<String>,
unset_env: Vec<String>,
env: Vec<(String, String)>,
}
impl PlannedCommand {
fn new(program: impl Into<String>) -> Self {
Self {
program: program.into(),
args: Vec::new(),
unset_env: Vec::new(),
env: Vec::new(),
}
}
fn arg(mut self, arg: impl Into<String>) -> Self {
self.args.push(arg.into());
self
}
fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env.push((key.into(), value.into()));
self
}
fn env_remove(mut self, key: impl Into<String>) -> Self {
self.unset_env.push(key.into());
self
}
fn to_shell_line(&self) -> String {
let mut parts = Vec::new();
if !self.unset_env.is_empty() {
parts.push("unset".to_string());
parts.extend(self.unset_env.iter().map(shell_arg));
parts.push("&&".to_string());
}
parts.extend(
self.env
.iter()
.map(|(key, value)| format!("{}={}", shell_arg(key), shell_arg(value))),
);
parts.push(shell_arg(&self.program));
parts.extend(self.args.iter().map(shell_arg));
parts.join(" ")
}
}
fn shell_arg(arg: impl AsRef<str>) -> String {
let arg = arg.as_ref();
if arg
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || "_-./:=@".contains(ch))
{
return arg.to_string();
}
format!("'{}'", arg.replace('\'', "'\\''"))
}
fn workspace_root() -> PathBuf {
let mut root = Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf();
root.pop();
root.pop();
root.pop();
root
}

View file

@ -18,8 +18,6 @@ struct Cli {
#[derive(Debug, Subcommand)]
enum Command {
/// Check source boundary rules.
CheckBoundary(commands::CheckBoundaryArgs),
/// Build Fabro Docker images with the release pipeline layout.
DockerBuild(commands::DockerBuildArgs),
/// Generate docs/reference/cli.mdx from the Fabro clap command tree.
@ -37,7 +35,6 @@ enum Command {
impl Command {
fn run(self) -> Result<()> {
match self {
Self::CheckBoundary(args) => commands::check_boundary(args),
Self::DockerBuild(args) => commands::docker_build(args),
Self::GenerateCliReference(args) => commands::generate_cli_reference(args),
Self::GenerateOptionsReference(args) => commands::generate_options_reference(args),

View file

@ -1,169 +0,0 @@
use std::fs;
use std::path::{Path, PathBuf};
fn fabro_dev() -> assert_cmd::Command {
assert_cmd::cargo::cargo_bin_cmd!("fabro-dev")
}
fn workspace_root() -> PathBuf {
let mut root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
root.pop();
root.pop();
root.pop();
root
}
#[expect(
clippy::disallowed_methods,
reason = "integration tests stage temporary Rust source fixtures with sync std::fs::write"
)]
fn write_file(root: &Path, path: &str, contents: &str) {
let path = root.join(path);
fs::create_dir_all(path.parent().expect("fixture path should have parent"))
.expect("creating fixture parent directory");
fs::write(path, contents).expect("writing fixture file");
}
fn check_boundary(root: &Path) -> assert_cmd::Command {
let mut cmd = fabro_dev();
cmd.args(["check-boundary", "--root"]).arg(root);
cmd
}
fn output_text(bytes: &[u8]) -> String {
String::from_utf8(bytes.to_vec()).expect("command output should be valid utf-8")
}
#[test]
fn current_tree_passes_boundary_check() {
let output = check_boundary(&workspace_root())
.assert()
.success()
.get_output()
.clone();
assert!(
output_text(&output.stdout).contains("CLI/server boundary checks passed."),
"success output should report boundary check pass"
);
}
#[test]
fn empty_workspace_passes() {
let fixture = tempfile::tempdir().expect("creating fixture");
check_boundary(fixture.path()).assert().success();
}
#[test]
fn allowed_symbols_pass() {
let fixture = tempfile::tempdir().expect("creating fixture");
write_file(
fixture.path(),
"lib/crates/fabro-cli/src/local_server.rs",
"fn resolve() { fabro_config::resolve_server(); }\n",
);
write_file(
fixture.path(),
"lib/crates/fabro-cli/src/command_context.rs",
"fn storage() { Storage::new(path); }\n",
);
check_boundary(fixture.path()).assert().success();
}
#[test]
fn temporary_exemption_marker_allows_listed_file() {
let fixture = tempfile::tempdir().expect("creating fixture");
write_file(
fixture.path(),
"lib/crates/fabro-cli/src/commands/pr/mod.rs",
"// boundary-exempt(pr-api): remove with follow-up #1\nfn storage() { Storage::new(path); }\n",
);
check_boundary(fixture.path()).assert().success();
}
#[test]
fn offending_file_reports_path_and_line() {
let fixture = tempfile::tempdir().expect("creating fixture");
write_file(
fixture.path(),
"lib/crates/fabro-cli/src/commands/bad.rs",
"fn storage() { Storage::new(path); }\n",
);
let output = check_boundary(fixture.path())
.assert()
.failure()
.code(1)
.get_output()
.clone();
let stderr = output_text(&output.stderr);
assert!(
stderr.contains(
"boundary check failed: Storage::new used outside allowlist: \
lib/crates/fabro-cli/src/commands/bad.rs:1"
),
"stderr should name offending file and line:\n{stderr}"
);
}
#[test]
fn multiple_offending_files_are_all_reported() {
let fixture = tempfile::tempdir().expect("creating fixture");
write_file(
fixture.path(),
"lib/crates/fabro-cli/src/commands/bad_storage.rs",
"fn storage() { Storage::new(path); }\n",
);
write_file(
fixture.path(),
"lib/crates/fabro-cli/src/commands/bad_server.rs",
"fn server() { ServerSettings::resolve(layer); }\n",
);
let output = check_boundary(fixture.path())
.assert()
.failure()
.code(1)
.get_output()
.clone();
let stderr = output_text(&output.stderr);
assert!(
stderr.contains("bad_storage.rs:1"),
"stderr should report storage offender:\n{stderr}"
);
assert!(
stderr.contains("bad_server.rs:1"),
"stderr should report server-settings offender:\n{stderr}"
);
}
#[test]
fn unexpected_temporary_exemption_marker_fails() {
let fixture = tempfile::tempdir().expect("creating fixture");
write_file(
fixture.path(),
"lib/crates/fabro-cli/src/commands/bad.rs",
"// boundary-exempt(pr-api): remove with follow-up #1\n",
);
let output = check_boundary(fixture.path())
.assert()
.failure()
.code(1)
.get_output()
.clone();
let stderr = output_text(&output.stderr);
assert!(
stderr.contains(
"boundary check failed: unexpected temporary exemption marker in \
lib/crates/fabro-cli/src/commands/bad.rs:1"
),
"stderr should report unexpected marker:\n{stderr}"
);
}

View file

@ -1,10 +1,4 @@
fn fabro_dev() -> assert_cmd::Command {
assert_cmd::cargo::cargo_bin_cmd!("fabro-dev")
}
fn output_text(bytes: &[u8]) -> String {
String::from_utf8(bytes.to_vec()).expect("command output should be valid utf-8")
}
use super::{fabro_dev, output_text};
#[test]
fn help_lists_docker_build_flags() {

View file

@ -1,32 +1,6 @@
use std::fs;
use std::path::Path;
fn fabro_dev() -> assert_cmd::Command {
assert_cmd::cargo::cargo_bin_cmd!("fabro-dev")
}
fn output_text(bytes: &[u8]) -> String {
String::from_utf8(bytes.to_vec()).expect("command output should be valid utf-8")
}
#[expect(
clippy::disallowed_methods,
reason = "integration tests stage temporary CLI reference fixtures with sync std::fs::write"
)]
fn write_file(root: &Path, path: &str, contents: &str) {
let path = root.join(path);
fs::create_dir_all(path.parent().expect("fixture path should have parent"))
.expect("creating fixture parent directory");
fs::write(path, contents).expect("writing fixture file");
}
#[expect(
clippy::disallowed_methods,
reason = "integration tests inspect generated CLI reference fixtures with sync std::fs::read_to_string"
)]
fn read_file(root: &Path, path: &str) -> String {
fs::read_to_string(root.join(path)).expect("reading fixture file")
}
use super::{fabro_dev, output_text, read_file, write_file};
fn cli_reference(root: &Path) -> assert_cmd::Command {
let mut cmd = fabro_dev();

View file

@ -1,32 +1,6 @@
use std::fs;
use std::path::Path;
fn fabro_dev() -> assert_cmd::Command {
assert_cmd::cargo::cargo_bin_cmd!("fabro-dev")
}
fn output_text(bytes: &[u8]) -> String {
String::from_utf8(bytes.to_vec()).expect("command output should be valid utf-8")
}
#[expect(
clippy::disallowed_methods,
reason = "integration tests stage temporary options reference fixtures with sync std::fs::write"
)]
fn write_file(root: &Path, path: &str, contents: &str) {
let path = root.join(path);
fs::create_dir_all(path.parent().expect("fixture path should have parent"))
.expect("creating fixture parent directory");
fs::write(path, contents).expect("writing fixture file");
}
#[expect(
clippy::disallowed_methods,
reason = "integration tests inspect generated options reference fixtures with sync std::fs::read_to_string"
)]
fn read_file(root: &Path, path: &str) -> String {
fs::read_to_string(root.join(path)).expect("reading fixture file")
}
use super::{fabro_dev, output_text, read_file, write_file};
fn options_reference(root: &Path) -> assert_cmd::Command {
let mut cmd = fabro_dev();

View file

@ -1,7 +1,6 @@
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
mod check_boundary;
mod docker_build;
mod generate_cli_reference;
mod generate_options_reference;
@ -24,6 +23,25 @@ fn output_text(bytes: &[u8]) -> String {
String::from_utf8(bytes.to_vec()).expect("command output should be valid utf-8")
}
#[expect(
clippy::disallowed_methods,
reason = "integration tests stage temporary fixture files with sync std::fs::write"
)]
fn write_file(root: &Path, path: &str, contents: impl AsRef<[u8]>) {
let path = root.join(path);
std::fs::create_dir_all(path.parent().expect("fixture path should have parent"))
.expect("creating fixture parent directory");
std::fs::write(path, contents).expect("writing fixture file");
}
#[expect(
clippy::disallowed_methods,
reason = "integration tests inspect fixture files with sync std::fs::read_to_string"
)]
fn read_file(root: &Path, path: &str) -> String {
std::fs::read_to_string(root.join(path)).expect("reading fixture file")
}
#[expect(
clippy::disallowed_methods,
reason = "integration test intentionally shells out to Cargo to verify the cargo dev alias"
@ -48,7 +66,6 @@ fn help_lists_scaffolded_commands() {
let stdout = output_text(&output.stdout);
for command in [
"check-boundary",
"docker-build",
"generate-cli-reference",
"generate-options-reference",
@ -76,29 +93,11 @@ fn cargo_dev_alias_resolves_to_fabro_dev_help() {
let stdout = output_text(&output.stdout);
assert!(
stdout.contains("check-boundary"),
stdout.contains("docker-build"),
"cargo dev help should come from fabro-dev:\n{stdout}"
);
}
#[test]
fn check_boundary_help_succeeds() {
let output = cargo_dev(&["check-boundary", "--help"]);
assert!(
output.status.success(),
"cargo dev check-boundary --help failed\nstdout:\n{}\nstderr:\n{}",
output_text(&output.stdout),
output_text(&output.stderr)
);
let stdout = output_text(&output.stdout);
assert!(
stdout.contains("Check source boundary rules"),
"check-boundary help should describe the subcommand:\n{stdout}"
);
}
#[test]
fn unknown_subcommand_exits_with_clap_usage_error() {
let output = fabro_dev()

View file

@ -1,21 +1,7 @@
use std::path::Path;
use std::process::Command;
fn fabro_dev() -> assert_cmd::Command {
assert_cmd::cargo::cargo_bin_cmd!("fabro-dev")
}
fn output_text(bytes: &[u8]) -> String {
String::from_utf8(bytes.to_vec()).expect("command output should be valid utf-8")
}
#[expect(
clippy::disallowed_methods,
reason = "integration tests stage temporary release fixture repositories with sync std::fs::write"
)]
fn write_file(path: &Path, contents: &str) {
std::fs::write(path, contents).expect("writing fixture file");
}
use super::{fabro_dev, output_text, write_file};
#[expect(
clippy::disallowed_methods,
@ -39,7 +25,8 @@ fn git(root: &Path, args: &[&str]) {
fn release_fixture() -> tempfile::TempDir {
let fixture = tempfile::tempdir().expect("creating fixture");
write_file(
&fixture.path().join("Cargo.toml"),
fixture.path(),
"Cargo.toml",
r#"[workspace]
members = []
@ -198,7 +185,7 @@ fn dry_run_reports_skip_tests_without_running_release_tests() {
#[test]
fn dirty_worktree_errors_unless_dry_run() {
let fixture = release_fixture();
write_file(&fixture.path().join("dirty.txt"), "dirty\n");
write_file(fixture.path(), "dirty.txt", "dirty\n");
let output = fabro_dev()
.args([

View file

@ -1,23 +1,4 @@
use std::path::Path;
fn fabro_dev() -> assert_cmd::Command {
assert_cmd::cargo::cargo_bin_cmd!("fabro-dev")
}
fn output_text(bytes: &[u8]) -> String {
String::from_utf8(bytes.to_vec()).expect("command output should be valid utf-8")
}
#[expect(
clippy::disallowed_methods,
reason = "integration tests stage temporary SPA fixture files with sync std::fs::write"
)]
fn write_file(root: &Path, path: &str, contents: &[u8]) {
let path = root.join(path);
std::fs::create_dir_all(path.parent().expect("fixture path should have parent"))
.expect("creating fixture parent directory");
std::fs::write(path, contents).expect("writing fixture file");
}
use super::{fabro_dev, output_text, write_file};
#[test]
fn refresh_spa_mirrors_dist_and_removes_source_maps() {