88598c6f7e
# Conflicts: # context/test-evidence/command.md # crates/improvise-command/src/ui/app.rs # tests/shipped_documents_load.rs
334 lines
12 KiB
Rust
334 lines
12 KiB
Rust
//! 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<PathBuf> {
|
|
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<PathBuf> = 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::<Vec<_>>()
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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(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)
|
|
);
|
|
}
|