git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

tests/user_paths.rs (5K)

  1 //! The crate's own source, checked for paths that belong to the user.
  2 //!
  3 //! An integration test on purpose, for the reason `doc_claims.rs` gives about
  4 //! its own scans: anything under `src/` is counted by them, and a scanner
  5 //! living there is the first thing it finds. From out here the needles can be
  6 //! written plainly, because this file is not in what it reads.
  7 //!
  8 //! Deliberately MIRRORED per crate rather than shared from a helper: each is
  9 //! its own git repository that must build standalone, and this needs nothing
 10 //! but std. Same call `doc_claims.rs` makes.
 11 
 12 pub mod user_paths {
 13     use std::path::{Path, PathBuf};
 14 
 15     /// Every `.rs` file under `src/`, as (path, contents).
 16     fn sources(root: &Path) -> Vec<(PathBuf, String)> {
 17         fn walk(dir: &Path, out: &mut Vec<(PathBuf, String)>) {
 18             let Ok(entries) = std::fs::read_dir(dir) else { return };
 19             for e in entries.flatten() {
 20                 let p = e.path();
 21                 if p.is_dir() {
 22                     walk(&p, out);
 23                 } else if p.extension().and_then(|x| x.to_str()) == Some("rs") {
 24                     if let Ok(s) = std::fs::read_to_string(&p) {
 25                         out.push((p, s));
 26                     }
 27                 }
 28             }
 29         }
 30         let mut v = Vec::new();
 31         walk(&root.join("src"), &mut v);
 32         v
 33     }
 34 
 35     /// Prose says what the code used to do, so only code counts.
 36     fn is_comment(line: &str) -> bool {
 37         let t = line.trim_start();
 38         t.starts_with("//") || t.starts_with("*")
 39     }
 40 
 41     /// `cargo test` must not touch anything the user owns. Two shapes of that
 42     /// shipped from this crate, and this is the scan that would have caught
 43     /// either the day it was written.
 44     ///
 45     /// **A config path built by hand.** `DesignSettings::file_path` spelled
 46     /// out `$HOME/.config/cce/cce-designer/state.kdl`. `State::new` loads the
 47     /// bundled project, whose meta subnets overwrite the live viewport flags,
 48     /// so every test that reached `save_settings` wrote the bundled project's
 49     /// show_grid / show_cube / show_origin over the user's own. The suite
 50     /// reset three of their toggles on every run and stayed green. The path
 51     /// goes through `cce_ui::config::cce_config_dir()` now, which is what the
 52     /// `cfg(test)` redirect keys off — so hand-assembling one out of
 53     /// `.config` is precisely the regression to refuse, and going through the
 54     /// toolkit is the only way in.
 55     ///
 56     /// **A scratch path shared with the rest of the machine.** Two tests kept
 57     /// their files at fixed `/tmp` names. /tmp is one namespace shared by
 58     /// every user — the point `../cce-compositor/WORKSPACE.md` makes about
 59     /// `cce_runtime_dir` — so a fixed name belongs to whoever ran first and
 60     /// the sticky bit denies it to everyone else; nearer to hand, two
 61     /// checkouts running their suites at once shared one directory. Scoping
 62     /// by pid is the crate's own convention, followed at every other site.
 63     ///
 64     /// The rule is on `temp_dir()` rather than on tests alone because a fixed
 65     /// /tmp path is no better in shipped code; if one ever needs an unscoped
 66     /// name it should earn a line here saying why.
 67     #[test]
 68     fn no_source_builds_a_path_the_user_owns() {
 69         let root = Path::new(env!("CARGO_MANIFEST_DIR"));
 70         let files = sources(root);
 71         assert!(!files.is_empty(), "found no sources under src/ — the walk is broken");
 72 
 73         let mut bad: Vec<String> = Vec::new();
 74         for (path, src) in &files {
 75             let name = path.strip_prefix(root).unwrap_or(path).display();
 76             let lines: Vec<&str> = src.lines().collect();
 77             for (i, line) in lines.iter().enumerate() {
 78                 if is_comment(line) {
 79                     continue;
 80                 }
 81                 if line.contains("\".config\"") || line.contains("/.config") {
 82                     bad.push(format!(
 83                         "{name}:{}: builds a config path by hand — go through \
 84                          cce_ui::config::cce_config_dir(), which is what the \
 85                          cfg(test) redirect keys off:\n      {}",
 86                         i + 1,
 87                         line.trim()
 88                     ));
 89                 }
 90                 // The call may wrap, so the scoping can be a line or two down.
 91                 if line.contains("temp_dir()") {
 92                     let window = lines[i..lines.len().min(i + 3)].join(" ");
 93                     if !window.contains("process::id()") {
 94                         bad.push(format!(
 95                             "{name}:{}: a scratch path shared with every other \
 96                              user and every concurrent run — scope it with \
 97                              std::process::id():\n      {}",
 98                             i + 1,
 99                             line.trim()
100                         ));
101                     }
102                 }
103             }
104         }
105         assert!(bad.is_empty(), "source builds paths the user owns:\n  {}", bad.join("\n  "));
106     }
107 }