//! Every `.improv` document this repository ships must load (improvise-odbc). //! "Shipped" is "tracked", read off git's index: `context/test-evidence/tui.md`. use std::path::{Path, PathBuf}; /// Every tracked `.improv` and `.improv.gz` under `root`. Outside a checkout this /// panics rather than answering empty (improvise-c0aj). fn shipped_documents(root: &Path) -> Vec { let out = git(root) .args(["ls-files", "-z", "--", "*.improv", "*.improv.gz"]) .output() .expect("git runs: this test reads the index, so it needs a checkout"); assert!( out.status.success(), "git ls-files failed in {}: {}", root.display(), String::from_utf8_lossy(&out.stderr) ); let mut found: Vec = String::from_utf8(out.stdout) .expect("UTF-8 paths") .split('\0') .filter(|p| !p.is_empty()) .map(|p| root.join(p)) .collect(); found.sort(); found } #[test] fn every_shipped_document_loads() { let root = Path::new(env!("CARGO_MANIFEST_DIR")); let documents = shipped_documents(root); assert!( !documents.is_empty(), "precondition: the index must list at least one shipped document" ); for path in documents { let workbook = improvise::persistence::load(&path) .unwrap_or_else(|e| panic!("{} does not load: {e}", path.display())); assert!( workbook.views.contains_key(&workbook.active_view), "{} loads but its initial view {:?} is not one of {:?}", path.display(), workbook.active_view, workbook.views.keys().collect::>() ); } } /// improvise-gk51: in every shipped document, pivot and records mode, a cell /// projected as a `Record` is one the host would accept a write to. #[test] fn every_record_a_shipped_document_projects_is_writable() { use improvise::command::key::{KeyCode, KeyEvent, KeyModifiers}; use improvise::protocol::CellId; use improvise::ui::app::App; use improvise_command::ui::project::{Viewport, grid_of}; let root = Path::new(env!("CARGO_MANIFEST_DIR")); let mut records = 0; let mut refused = Vec::new(); for path in shipped_documents(root) { let workbook = improvise::persistence::load(&path).expect("every shipped document loads"); let mut app = App::new(workbook, None); for _mode in ["pivot", "records"] { let whole = Viewport { row_offset: 0, col_offset: 0, rows: app.layout.row_count(), cols: app.layout.col_count(), }; let grid = grid_of(&app, whole); for cell in grid.cells.iter().flatten() { if let Some(CellId::Record(record)) = &cell.id { records += 1; if let Some(why) = app.unwritable(app.view_name(), record) { refused.push(format!("{}: {why}", path.display())); } } } let r = KeyEvent::new(KeyCode::Char('R'), KeyModifiers::NONE); let _ = app.handle_key(r); } } assert!( records > 0, "precondition: the shipped documents project records" ); assert!( refused.is_empty(), "projected as Record, then refused: {refused:?}" ); } /// Settings that change how git reads a pathspec or finds attributes. const PATHSPEC_AND_ATTRIBUTE_SETTINGS: [&str; 5] = [ "GIT_LITERAL_PATHSPECS", "GIT_GLOB_PATHSPECS", "GIT_NOGLOB_PATHSPECS", "GIT_ICASE_PATHSPECS", "GIT_ATTR_SOURCE", ]; /// `git -C root`, with every repo-local variable a caller exported removed, /// since `GIT_DIR` and friends override `-C` (improvise-53zz). fn git(root: &Path) -> std::process::Command { let names = untraced_git() .args(["rev-parse", "--local-env-vars"]) .output() .expect("git runs"); let mut git = untraced_git(); for name in String::from_utf8(names.stdout).expect("UTF-8").lines() { git.env_remove(name); } // Process-wide, so `--local-env-vars` lists none of them (improvise-a0i3). for name in PATHSPEC_AND_ATTRIBUTE_SETTINGS { git.env_remove(name); } git.arg("-C").arg(root); git } /// `git` with every `GIT_TRACE*` variable removed: one pointed at stdout writes /// into what we parse. A prefix, since the family is open-ended (improvise-2iws). fn untraced_git() -> std::process::Command { let mut git = std::process::Command::new("git"); for (name, _) in std::env::vars_os() { if name.to_string_lossy().starts_with("GIT_TRACE") { git.env_remove(name); } } // A config `trace2.*Target` survives a removal; `0` here outranks it (vav0). for name in TRACE2_TARGETS { git.env(name, "0"); } git } /// The trace2 targets a config file can also set, as `trace2.normalTarget`, /// `trace2.eventTarget` and `trace2.perfTarget`. const TRACE2_TARGETS: [&str; 3] = ["GIT_TRACE2", "GIT_TRACE2_EVENT", "GIT_TRACE2_PERF"]; /// A scratch repository, so each way of shipping a document is set up for real. fn scratch_repo() -> tempfile::TempDir { let dir = tempfile::tempdir().expect("a temp dir"); run_git(dir.path(), &["init", "-q", "--template="]); dir } fn run_git(root: &Path, args: &[&str]) { let status = git(root) .env("GIT_CONFIG_GLOBAL", "/dev/null") .env("GIT_CONFIG_NOSYSTEM", "1") .args(["-c", "core.excludesFile=/dev/null"]) .args(["-c", "core.attributesFile=/dev/null"]) .args(args) .status() .expect("git runs"); assert!(status.success(), "git {args:?} failed"); } fn write(root: &Path, rel: &str, text: &str) { let path = root.join(rel); std::fs::create_dir_all(path.parent().expect("a parent")).expect("dirs"); std::fs::write(path, text).expect("writes"); } fn walked(root: &Path, rel: &str) -> bool { shipped_documents(root).contains(&root.join(rel)) } /// improvise-c0aj: one test per way of shipping a document that reading the /// root `.gitignore` missed. Evidence: `context/test-evidence/tui.md`. #[test] fn a_single_file_un_ignore_is_walked() { let repo = scratch_repo(); let root = repo.path(); write(root, ".gitignore", "*.improv\n!web/demo.improv\n"); write(root, "web/demo.improv", "x"); run_git(root, &["add", ".gitignore", "web/demo.improv"]); assert!(walked(root, "web/demo.improv")); } #[test] fn a_gzipped_document_is_walked() { let repo = scratch_repo(); let root = repo.path(); write(root, ".gitignore", "*.improv\n"); write(root, "docs/model.improv.gz", "x"); run_git(root, &["add", ".gitignore", "docs/model.improv.gz"]); assert!(walked(root, "docs/model.improv.gz")); } #[test] fn a_document_un_ignored_by_a_nested_gitignore_is_walked() { let repo = scratch_repo(); let root = repo.path(); write(root, ".gitignore", "*.improv\n"); write(root, "harness/.gitignore", "!keep.improv\n"); write(root, "harness/keep.improv", "x"); run_git( root, &[ "add", ".gitignore", "harness/.gitignore", "harness/keep.improv", ], ); assert!(walked(root, "harness/keep.improv")); } /// An empty walk would pass every per-document assertion vacuously, so a /// directory git cannot read must stop the test instead. #[test] #[should_panic(expected = "git ls-files failed")] fn a_directory_that_is_not_a_checkout_fails_loudly() { let not_a_repo = tempfile::tempdir().expect("a temp dir"); shipped_documents(not_a_repo.path()); } #[test] fn a_force_added_document_is_walked() { let repo = scratch_repo(); let root = repo.path(); write(root, ".gitignore", "*.improv\n"); write(root, "secret/forced.improv", "x"); run_git(root, &["add", ".gitignore"]); run_git(root, &["add", "-f", "secret/forced.improv"]); assert!(walked(root, "secret/forced.improv")); } /// Runs one of this binary's own tests in a child process with `env` added, /// since the environment is what is under test and threads share ours. fn rerun_with_env(test: &str, env: &[(&str, &Path)]) -> std::process::Output { let mut child = std::process::Command::new(std::env::current_exe().expect("test binary")); child.args([test, "--exact", "--nocapture"]); for (name, value) in env { child.env(name, value); } let out = child.output().expect("the test binary runs"); let stdout = String::from_utf8_lossy(&out.stdout); assert!( stdout.contains("running 1 test"), "{test} did not run: {}", told(&out) ); out } /// Everything a child test said, both streams: its panic lands on stderr. fn told(out: &std::process::Output) -> String { let stdout = String::from_utf8_lossy(&out.stdout); let stderr = String::from_utf8_lossy(&out.stderr); format!("\n--- stdout\n{stdout}\n--- stderr\n{stderr}") } /// Every hostile git setting these tests have met, at once, in one child per /// test (improvise-eahe). Each setting's own witness is a rival: `context/test-evidence/tui.md`. #[test] fn a_hostile_git_environment_changes_nothing() { // improvise-pr3i: a template whose `info/exclude` refuses `*.gz`. let templates = tempfile::tempdir().expect("a temp dir"); write(templates.path(), "info/exclude", "*.gz\n"); // improvise-53zz: a global and a system config whose excludes refuse `*.gz`. let home = tempfile::tempdir().expect("a temp dir"); let excludes = home.path().join("ignore"); std::fs::write(&excludes, "*.gz\n").expect("writes"); let config = home.path().join("gitconfig"); // vav0: trace2 targets go to stdout. 3j70: a bad sharedRepository kills `init` // and `add` but not `ls-files`, and no `-c` in run_git overrides it. let text = format!( "[core]\n\texcludesFile = {}\n[trace2]\n\tnormalTarget = /dev/stdout\n\teventTarget = /dev/stdout\n\tperfTarget = /dev/stdout\n[core]\n\tsharedRepository = nonsense\n", excludes.display() ); std::fs::write(&config, text).expect("writes"); // improvise-vwjj: git's default ignore and attributes files, no config line. let xdg = tempfile::tempdir().expect("a temp dir"); write(xdg.path(), "git/ignore", "*.gz\n"); write( xdg.path(), "git/attributes", "*.gz working-tree-encoding=UTF-16\n", ); // improvise-53zz: an inherited `GIT_DIR`, which beats `-C`. let outer = scratch_repo(); let dot_git = outer.path().join(".git"); let index = dot_git.join("index"); let stdout = Path::new("/dev/stdout"); let one = Path::new("1"); let hostile: Vec<(&str, &Path)> = vec![ ("GIT_TEMPLATE_DIR", templates.path()), ("GIT_CONFIG_GLOBAL", &config), ("GIT_CONFIG_SYSTEM", &config), ("XDG_CONFIG_HOME", xdg.path()), ("GIT_DIR", &dot_git), ("GIT_INDEX_FILE", &index), // improvise-a0i3: process-wide pathspec and attribute settings. ("GIT_LITERAL_PATHSPECS", one), ("GIT_GLOB_PATHSPECS", one), ("GIT_NOGLOB_PATHSPECS", one), ("GIT_ICASE_PATHSPECS", one), ("GIT_ATTR_SOURCE", Path::new("HEAD")), // improvise-2iws: every trace that writes to its target on git 2.55. ("GIT_TRACE", stdout), ("GIT_TRACE_SETUP", stdout), ("GIT_TRACE_PERFORMANCE", stdout), ("GIT_TRACE2", stdout), ("GIT_TRACE2_EVENT", stdout), ("GIT_TRACE2_PERF", stdout), ]; for test in [ "a_gzipped_document_is_walked", "every_shipped_document_loads", ] { let out = rerun_with_env(test, &hostile); assert!( out.status.success(), "{test} fails in a hostile git environment: {}", told(&out) ); } let staged = git(outer.path()) .args(["ls-files"]) .output() .expect("git runs"); assert!( staged.stdout.is_empty(), "the outer index gained: {}", String::from_utf8_lossy(&staged.stdout) ); }