//! The three rules in `docs/module-layout.md`, checked, and the one //! constraint its `config/` section adds. //! //! Three of the four cannot be expressed as visibility, which is why this //! file exists rather than being a `pub(…)` somewhere: //! //! - **`lexicon/` imports nothing from `clients/`** — `clients` has to be //! `pub(crate)` for `cmd/` to call it, and `pub(crate)` is visible to //! `lexicon/` too. There is no modifier for "visible to `cmd` but not //! `lexicon`" inside one crate; only a workspace split would do it. //! - **`clients/git/` owns every interaction with git** — a `Command` is //! `std`'s, and no modifier on anything in this crate can stop another //! module building one of its own. //! - **No client asks `config/` who we are** — same shape, same reason. //! //! The remaining rule — *nothing outside `cmd/` imports `cmd/`* — **is** //! enforced by the compiler now: everything in `cmd/` is `pub(super)`, `pub(in //! crate::cmd)` or, for the entry points `main.rs` dispatches through, //! `pub(crate)`. It is checked here anyway, because the compiler only stops //! the ones that are not `pub(crate)`, and the entry points are. //! //! # Why a test and not a review habit //! //! Every one of these was violated in the tree at some point while nobody was //! looking, and each was found by grep rather than by anything failing. A rule //! that only a careful reader enforces is a rule that holds until the day it //! matters. use std::path::{Path, PathBuf}; /// `src/`, from wherever cargo runs this. fn src() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("src") } fn rust_files(dir: &Path, out: &mut Vec) { let entries = std::fs::read_dir(dir).unwrap_or_else(|e| panic!("read {}: {e}", dir.display())); for entry in entries.filter_map(Result::ok) { let path = entry.path(); if path.is_dir() { rust_files(&path, out); } else if path.extension().is_some_and(|e| e == "rs") { out.push(path); } } } /// The code, with the prose taken out. /// /// This tree documents itself heavily and names module paths in prose /// constantly — `crate::cmd::pr::write` appears in a dozen doc comments that /// are not imports. Comments are therefore dropped before anything is /// matched, or every rule below would fail on its own explanation. /// /// A `//` is only treated as a comment when it is not preceded by `:`, which /// keeps `at://` and `https://` inside string literals intact. fn code_only(text: &str) -> String { let mut out = String::with_capacity(text.len()); for line in text.lines() { if line.trim_start().starts_with("//") { continue; } let mut cut = line.len(); let bytes = line.as_bytes(); for i in 0..line.len().saturating_sub(1) { if bytes[i] == b'/' && bytes[i + 1] == b'/' && (i == 0 || bytes[i - 1] != b':') { cut = i; break; } } out.push_str(&line[..cut]); out.push('\n'); } out } /// Every file under `src/` whose code mentions `needle`. fn offenders(folder: &str, needle: &str) -> Vec { let mut files = Vec::new(); rust_files(&src().join(folder), &mut files); files.sort(); files .into_iter() .filter(|p| code_only(&std::fs::read_to_string(p).unwrap()).contains(needle)) .map(|p| { p.strip_prefix(src().parent().unwrap()) .unwrap_or(&p) .display() .to_string() }) .collect() } /// **Rule 1.** `lexicon/` is the pure layer — syntax, record shapes, /// conventions. The moment it opens a socket it stops being testable without /// one, and every test of a *caller* becomes a test of the network. #[test] fn lexicon_imports_nothing_from_clients() { assert!( offenders("lexicon", "crate::clients").is_empty(), "lexicon/ must stay pure; found: {:?}", offenders("lexicon", "crate::clients") ); } /// **Rule 2.** A command is the top of the tree. Anything two commands both /// need belongs lower down, not somewhere to reach up for. /// /// No exceptions. There was one — `clients/atproto/oauth/pages.rs` reached /// into `cmd::about` for the field of DNA behind the post-login page — and it /// went away when the page and the field moved to `html/` and `art.rs`. This /// test carried it as a named allowlist entry that failed once it stopped /// being needed, which is how it came to be deleted rather than forgotten. #[test] fn nothing_outside_cmd_imports_cmd() { let found = ["clients", "config", "lexicon", "logging", "term", "html"] .into_iter() .flat_map(|folder| offenders(folder, "crate::cmd")) .collect::>(); assert!(found.is_empty(), "these reach up into cmd/: {found:?}"); } /// **The `config/` constraint**, which is not one of the page's three rules /// and was labelled as its third until the real one landed below: no client /// asks `config/` who we are. /// /// A client takes the account, the DID or the session as an argument, from /// the command that already knows. `config::dir` is deliberately not covered: /// a path is not an identity, and `clients/git/ssh.rs` uses it to park a /// matched push key beside the session store. /// /// The difference is testability. A client handed a DID can be driven with /// any DID; a client that looks one up can only be driven by the machine it /// is running on. #[test] fn no_client_asks_config_who_we_are() { let found = offenders("clients", "crate::config::account"); assert!( found.is_empty(), "a client reached for the current account instead of being handed it: {found:?}" ); } /// **Rule 3**, which nothing checked until now. `clients/git/` owns every /// interaction with git: nothing else builds a `Command::new("git")`. /// /// It was a claim in two doc comments and true only by inspection, and it /// stopped being true at least once — `cmd/report.rs` ran `git --version` /// through a `Command` of its own for the version line in a bug report. /// Harmless in itself, and exactly the crack the next one would have gone /// through: [`atgc logs git`](../src/logging/git.rs) records every git /// subprocess by sitting under the single `spawn` in `clients/git/run.rs`, /// so a caller that builds its own is a git process nothing records. /// /// `testutil` is the one exception the page names, and it is scaffolding /// rather than a caller: its repos are built for tests to run *against*, and /// routing them through the client under test would make the fixture depend /// on the thing being fixed. #[test] fn nothing_outside_clients_git_builds_a_git_command() { let mut files = Vec::new(); rust_files(&src(), &mut files); files.sort(); let found: Vec = files .into_iter() .filter(|p| !p.starts_with(src().join("clients/git"))) .filter(|p| p.file_name().is_some_and(|n| n != "testutil.rs")) .filter(|p| { code_only(&std::fs::read_to_string(p).unwrap()).contains("Command::new(\"git\"") }) .map(|p| p.display().to_string()) .collect(); assert!( found.is_empty(), "these start git outside clients/git/, where nothing records it: {found:?}" ); } /// The comment stripper has to be right or every rule above passes for the /// wrong reason — a false negative here is silent. #[test] fn prose_is_stripped_but_code_and_urls_survive() { assert_eq!(code_only("//! see crate::cmd::pr\n").trim(), ""); assert_eq!(code_only("/// crate::cmd::pr\n").trim(), ""); assert_eq!(code_only(" // crate::cmd::pr\n").trim(), ""); assert_eq!( code_only("use crate::cmd::pr; // crate::cmd::stack\n").trim(), "use crate::cmd::pr;" ); // The one that would break it: `//` inside a URL is not a comment. assert_eq!( code_only("let uri = \"at://did:plc:abc/x\";\n").trim(), "let uri = \"at://did:plc:abc/x\";" ); }