use std::{
path::{Path, PathBuf},
process::Command,
};
use assert_cmd::cargo::CommandCargoExt;
fn git(dir: &Path, args: &[&str]) {
let out = Command::new("git")
.args([
"-c",
"user.name=t",
"-c",
"user.email=t@t",
"-c",
"commit.gpgsign=false",
"-c",
"tag.gpgsign=false",
])
.args(args)
.current_dir(dir)
.output()
.unwrap();
assert!(
out.status.success(),
"git {args:?}: {}",
String::from_utf8_lossy(&out.stderr)
);
}
const JHN: &str = r#"<usx version="3.0"><book code="JHN" style="id"/><chapter number="3" style="c"/><para style="p"><verse number="16" style="v"/>For God so loved <verse number="17" style="v"/>For God sent</para></usx>"#;
fn package(dir: &Path, id: &str, version: &str) {
std::fs::create_dir_all(dir.join("usx")).unwrap();
let manifest = format!(
"[package]\nid = \"{id}\"\nname = \"Test {id}\"\nabbreviation = \"{}\"\nversion = \"{version}\"\nlanguage = \"en\"\nlicense = \"CC0-1.0\"\n\n[formats]\nusx = {{ path = \"usx/\" }}\n",
id.to_uppercase()
);
std::fs::write(dir.join("logoxide.toml"), manifest).unwrap();
std::fs::write(dir.join("usx/JHN.usx"), JHN).unwrap();
}
fn released(dir: &Path, id: &str, version: &str) {
package(dir, id, version);
if !dir.join(".git").exists() {
git(dir, &["init", "-q", "-b", "main"]);
}
git(dir, &["add", "-A"]);
git(dir, &["commit", "-q", "-m", version]);
git(dir, &["tag", &format!("v{version}")]);
}
struct World {
root: PathBuf,
}
impl World {
fn new(name: &str) -> World {
let root = std::env::temp_dir().join(format!("lx-cli-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(root.join("home")).unwrap();
std::fs::write(root.join("config.toml"), "").unwrap();
World { root }
}
fn path(&self, p: &str) -> PathBuf {
self.root.join(p)
}
fn lx(&self, args: &[&str]) -> std::process::Output {
let vault = self.path("vault");
std::fs::create_dir_all(&vault).unwrap();
let mut command = Command::cargo_bin("lx").unwrap();
command
.args(args)
.current_dir(&vault)
.env("LOGOXIDE_HOME", self.path("library"))
.env("LOGOXIDE_CACHE", self.path("cache"))
.env("LOGOXIDE_CONFIG", self.path("config.toml"))
.env("HOME", self.path("home"))
.env("XDG_CONFIG_HOME", self.path("home/.config"))
.env("XDG_DATA_HOME", self.path("home/.local/share"))
.env("XDG_CACHE_HOME", self.path("home/.cache"))
.env("XDG_RUNTIME_DIR", self.path("runtime"))
.env("NO_PROXY", "127.0.0.1")
.env("no_proxy", "127.0.0.1");
command.output().unwrap()
}
fn lock_path(&self, folder: &Path) -> PathBuf {
logoxide::locking::lock_path_in(&self.path("runtime/logoxide"), folder)
}
fn ok(&self, args: &[&str]) -> String {
let out = self.lx(args);
assert!(
out.status.success(),
"lx {args:?}: {}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn json(&self, args: &[&str]) -> serde_json::Value {
let text = self.ok(args);
serde_json::from_str(&text).unwrap_or_else(|e| panic!("lx {args:?}: {e}: {text}"))
}
}
impl Drop for World {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.root);
}
}
#[test]
fn a_vault_adds_lists_turns_off_and_cleans() {
let world = World::new("vault");
let repo = world.path("tb");
released(&repo, "tb", "1.0.0");
let repo = repo.display().to_string();
assert!(world.ok(&["init"]).contains("created"));
assert!(world.ok(&["list"]).contains("nothing installed yet"));
assert_eq!(
world.json(&["list", "--format", "json"]),
serde_json::json!([])
);
let added = world.ok(&["add", "--git", &repo]);
assert!(added.contains("added tb 1.0.0"), "{added}");
assert!(world.path("vault/logoxide.lock").is_file());
let installed = world.ok(&["install"]);
assert!(installed.contains("1 packages, locked"), "{installed}");
let listed = world.ok(&["list"]);
assert!(
listed.contains("tb 1.0.0 TB (Test tb), CC0-1.0"),
"{listed}"
);
let listed = world.json(&["list", "--format", "json"]);
assert_eq!(listed[0]["id"], "tb");
assert_eq!(listed[0]["version"], "1.0.0");
assert_eq!(listed[0]["abbreviation"], "TB");
assert_eq!(listed[0]["enabled"], true);
assert_eq!(listed[0]["installed"], true);
assert_eq!(listed[0]["dependency"], false);
assert_eq!(listed[0]["git"], repo);
let info = world.json(&["info", "tb", "--format", "json"]);
assert_eq!(
(info["books"].as_u64(), info["verses"].as_u64()),
(Some(1), Some(17))
);
assert_eq!(info["unchanged"], true);
let on = world.lx(&["is-enabled", "tb"]);
assert_eq!(
(on.status.code(), on.stdout.as_slice()),
(Some(0), &b"on\n"[..])
);
assert!(world.ok(&["disable", "tb"]).contains("turned tb off"));
let off = world.lx(&["is-enabled", "tb"]);
assert_eq!(
(off.status.code(), off.stdout.as_slice()),
(Some(1), &b"off\n"[..])
);
assert_eq!(world.lx(&["is-enabled", "nothing"]).status.code(), Some(2));
assert!(world.ok(&["list"]).contains("(off)"));
assert!(world.ok(&["list", "--enabled"]).is_empty());
let disabled = world.json(&["list", "--disabled", "--format", "json"]);
assert_eq!(disabled[0]["enabled"], false);
world.ok(&["enable", "tb"]);
assert_eq!(world.lx(&["is-enabled", "tb"]).status.code(), Some(0));
assert!(
world
.ok(&["clean"])
.contains("every installed version is used")
);
world.ok(&["remove", "tb"]);
let unused = world.ok(&["clean"]);
assert!(unused.contains("unused: tb 1.0.0+g"), "{unused}");
assert!(
world
.ok(&["clean", "--remove"])
.contains("removed tb 1.0.0+g")
);
assert!(!world.path("library/packages/tb").exists());
assert!(
world
.ok(&["clean"])
.contains("every installed version is used")
);
}
#[test]
fn global_adds_to_the_library_and_list_all_shows_every_version() {
let world = World::new("global");
let (one, two) = (world.path("one"), world.path("two"));
released(&one, "one", "1.0.0");
released(&two, "two", "1.0.0");
world.ok(&["init"]);
world.ok(&["add", "--git", &one.display().to_string()]);
let added = world.ok(&["add", "--global", "--git", &two.display().to_string()]);
assert!(added.contains("library/logoxide.toml"), "{added}");
assert!(world.ok(&["list"]).contains("one 1.0.0"));
let global = world.ok(&["list", "--global"]);
assert!(
global.contains("two 1.0.0") && !global.contains("one"),
"{global}"
);
assert!(
!world
.lx(&["list", "--global", "--project", "."])
.status
.success()
);
let all = world.ok(&["list", "--all"]);
assert!(all.contains("one 1.0.0+g"), "{all}");
assert!(all.contains("(here)"), "{all}");
assert!(
all.contains("used by the library's own (--global)"),
"{all}"
);
let listed = world.json(&["list", "--all", "--format", "json"]);
assert_eq!(listed.as_array().map(Vec::len), Some(2));
assert_eq!(listed[1]["id"], "two");
assert_eq!(listed[1]["abbreviation"], "TWO");
assert_eq!(listed[1]["usedBy"][0]["library"], true);
world.ok(&["--global", "remove", "two"]);
let all = world.ok(&["list", "--all"]);
assert!(
all.contains("two 1.0.0+g") && all.contains("unused (lx clean --remove deletes it)"),
"{all}"
);
let listed = world.json(&["list", "--all", "--format", "json"]);
assert_eq!(listed[1]["usedBy"], serde_json::json!([]));
let old = world.path("library/packages/old/1.0.0+g0123abcd");
std::fs::create_dir_all(&old).unwrap();
std::fs::write(
old.join("logoxide.toml"),
"[package]\nid = \"old\"\nname = \"Old\"\nabbreviation = \"OLD\"\nversion = \"1.0.0\"\nlanguage = \"en\"\nupstream = \"a string, as it was\"\n",
)
.unwrap();
assert!(
world
.ok(&["list", "--all"])
.contains("old 1.0.0+g0123abcd (its logoxide.toml doesn't read) unused (delete ")
);
let cleaned = world.ok(&["clean", "--remove"]);
assert!(
cleaned.contains("kept old 1.0.0+g0123abcd: its logoxide.toml doesn't read"),
"{cleaned}"
);
assert!(cleaned.contains("removed two 1.0.0+g"), "{cleaned}");
assert!(old.join("logoxide.toml").is_file());
}
#[test]
fn path_packages_are_used_in_place() {
let world = World::new("path");
package(&world.path("local"), "local", "0.1.0");
world.ok(&["init"]);
let added = world.ok(&["add", "--path", "../local"]);
assert!(added.contains("added local 0.1.0"), "{added}");
let listed = world.ok(&["list"]);
assert!(listed.contains("path ../local"), "{listed}");
assert!(
world
.ok(&["clean"])
.contains("every installed version is used")
);
let listed = world.json(&["list", "--format", "json"]);
assert_eq!(listed[0]["path"], "../local");
}
#[test]
fn paths_are_kept_relative() {
let world = World::new("relative");
package(&world.path("local"), "local", "0.1.0");
world.ok(&["init"]);
let absolute = world.path("local").display().to_string();
let added = world.lx(&["add", "--path", &absolute]);
assert!(added.status.success());
assert!(!String::from_utf8_lossy(&added.stderr).contains("warning"));
let manifest = std::fs::read_to_string(world.path("vault/logoxide.toml")).unwrap();
assert!(manifest.contains("path = \"../local\""), "{manifest}");
std::fs::write(
world.path("vault/logoxide.toml"),
format!("[dependencies]\nlocal = {{ path = \"{absolute}\" }}\n"),
)
.unwrap();
let out = world.lx(&["install"]);
assert!(out.status.success());
let warned = String::from_utf8_lossy(&out.stderr);
assert!(warned.contains("is absolute"), "{warned}");
}
#[test]
fn check_reads_every_book() {
let world = World::new("check");
let dir = world.path("tb");
package(&dir, "tb", "1.0.0");
let dir = dir.display().to_string();
let checked = world.ok(&["check", &dir]);
assert!(checked.contains("1 books, 2 verses"), "{checked}");
let report = world.json(&["check", &dir, "--format", "json"]);
assert_eq!(report["ok"], true);
assert_eq!(
(report["books"].as_u64(), report["verses"].as_u64()),
(Some(1), Some(2))
);
assert_eq!(report["titles"][0]["path"], "usx/JHN.usx");
assert_eq!(report["skipped"], serde_json::json!([]));
let usx = |code: &str| format!(r#"<usx version="3.0"><book code="{code}" style="id"/></usx>"#);
std::fs::write(world.path("tb/usx/front.usx"), usx("FRT")).unwrap();
std::fs::write(world.path("tb/usx/ZZZ.usx"), usx("ZZZ")).unwrap();
let out = world.lx(&["check", &dir]);
assert!(out.status.success());
let said = String::from_utf8_lossy(&out.stderr);
assert!(
said.contains("note: usx/front.usx: FRT isn't a book"),
"{said}"
);
assert!(
said.contains("warning: usx/ZZZ.usx: ZZZ is a book"),
"{said}"
);
assert!(String::from_utf8_lossy(&out.stdout).contains("1 books, 2 verses"));
let report = world.json(&["check", &dir, "--format", "json"]);
assert_eq!(report["ok"], true);
assert_eq!(report["skipped"][0]["reason"], "unknown");
assert_eq!(report["skipped"][1]["reason"], "peripheral");
std::fs::write(world.path("tb/usx/john.usx"), JHN).unwrap();
let out = world.lx(&["check", &dir]);
assert_eq!(out.status.code(), Some(1));
let said = String::from_utf8_lossy(&out.stderr);
assert!(said.contains("usx/JHN.usx and usx/john.usx"), "{said}");
std::fs::remove_file(world.path("tb/usx/john.usx")).unwrap();
let manifest = std::fs::read_to_string(world.path("tb/logoxide.toml")).unwrap();
let eng = manifest.replace("language = \"en\"", "language = \"eng\"");
std::fs::write(world.path("tb/logoxide.toml"), &eng).unwrap();
let out = world.lx(&["check", &dir]);
assert!(out.status.success());
let said = String::from_utf8_lossy(&out.stderr);
assert!(
said.contains("warning: language `eng` isn't canonical BCP 47: use `en`"),
"{said}"
);
let report = world.json(&["check", &dir, "--format", "json"]);
assert_eq!(report["ok"], true);
assert!(report["warnings"][0].as_str().unwrap().contains("use `en`"));
std::fs::write(world.path("tb/logoxide.toml"), manifest).unwrap();
let map =
Path::new(env!("CARGO_MANIFEST_DIR")).join("../logoxide/tests/fixtures/english-original");
let checked = world.ok(&["check", &map.display().to_string()]);
assert!(
checked.contains("a versification map from English to Original"),
"{checked}"
);
let out = world.lx(&["check", &world.path("nothing").display().to_string()]);
assert_eq!(out.status.code(), Some(1));
}
#[test]
fn formats_are_only_where_a_command_has_them() {
let world = World::new("format");
world.ok(&["init"]);
let out = world.lx(&["remove", "x", "--format", "json"]);
assert_eq!(out.status.code(), Some(1));
assert!(String::from_utf8_lossy(&out.stderr).contains("--format json"));
assert_eq!(
world.lx(&["list", "--format", "tsv"]).status.code(),
Some(1)
);
let catalogs = world.json(&["catalog", "list", "--format", "json"]);
assert!(
catalogs["catalogs"]
.as_array()
.is_some_and(|c| !c.is_empty())
);
}
#[test]
fn catalog_text_is_printed_without_escapes() {
let world = World::new("escapes");
world.ok(&["init"]);
let catalog = world.path("catalog.toml");
std::fs::write(
&catalog,
"[[package]]\nid = \"kjv\"\nname = \"King James\\u001b]0;owned\\u0007 \\u001b[2JVersion\\u202e\"\nabbreviation = \"KJV\"\nlanguage = \"en\"\nlicense = \"CC0-1.0\\r\\u001b[1A\"\ngit = \"https://example.com/kjv\"\n",
)
.unwrap();
let out = world.ok(&["search", "kjv", "--catalog", catalog.to_str().unwrap()]);
assert!(out.contains("KJV (kjv): King James Version, "), "{out}");
assert!(
!out.contains(['\u{1b}', '\u{7}', '\r', '\u{202e}']),
"{out:?}"
);
}
#[test]
fn search_filters_and_groups_by_language() {
let world = World::new("languages");
world.ok(&["init"]);
let catalog = world.path("catalog.toml");
let package = |id: &str, name: &str, language: &str, language_name: &str| {
format!(
"[[package]]\nid = \"{id}\"\nname = \"{name}\"\nabbreviation = \"{}\"\nlanguage = \"{language}\"\nlanguage-name = \"{language_name}\"\nwork = \"bsb\"\ngit = \"https://example.com/{id}\"\n\n",
id.to_uppercase()
)
};
std::fs::write(
&catalog,
[
package("bsb", "Berean Standard Bible", "en", "English"),
package("bsb-es", "Biblia Estándar de Berea", "es", "Español"),
package("bsb-us", "Berean Standard Bible (US)", "en-US", "English"),
package("bsb-fr", "Bible Standard de Bérée", "fr", "Français"),
]
.concat(),
)
.unwrap();
let catalog = catalog.to_str().unwrap();
let ids = |args: &[&str]| -> Vec<String> {
let mut args = args.to_vec();
args.extend(["--catalog", catalog, "--json"]);
(world.json(&args).as_array().unwrap().iter())
.map(|e| e["id"].as_str().unwrap().to_string())
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>()
};
assert_eq!(ids(&["search", "--language", "en"]), ["bsb", "bsb-us"]);
assert_eq!(ids(&["search", "--language", "eng"]), ["bsb", "bsb-us"]);
assert_eq!(ids(&["search", "--language", "en-US"]), ["bsb-us"]);
assert_eq!(
ids(&["search", "--language", "espanol", "--language", "French"]),
["bsb-es", "bsb-fr"]
);
let out = world.ok(&["search", "--catalog", catalog, "--by-language"]);
let english = out.find("English (en): 2").expect(&out);
let spanish = out.find("Spanish (es): 1").expect(&out);
assert!(out.contains("Biblia Estándar de Berea, Español"), "{out}");
assert!(english < spanish && out.contains("French (fr): 1"), "{out}");
assert!(
out.contains(" Berean Standard Bible (work `bsb`): 2 editions"),
"{out}"
);
assert!(out.contains(" BSB-US (bsb-us)"), "{out}");
}
#[test]
fn catalog_ids_are_checked_and_shadowing_is_said() {
let world = World::new("ids");
world.ok(&["init"]);
let catalog = world.path("catalog.toml");
std::fs::write(
&catalog,
"[[package]]\nid = \"../kjv\"\nname = \"Bad\"\nabbreviation = \"BAD\"\nlanguage = \"en\"\ngit = \"https://example.com/bad\"\n\n[[package]]\nid = \"kjv\"\nname = \"King James Version\"\nabbreviation = \"KJV\"\nlanguage = \"en\"\ngit = \"https://example.com/kjv\"\n",
)
.unwrap();
let out = world.lx(&["search", "--catalog", catalog.to_str().unwrap()]);
let (stdout, stderr) = (
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
assert!(out.status.success(), "{stderr}");
assert!(
stdout.contains("KJV (kjv)") && !stdout.contains("BAD"),
"{stdout}"
);
assert!(
stderr.contains("warning: ") && stderr.contains("passed over package `../kjv`"),
"{stderr}"
);
released(&world.path("a/tb"), "tb", "1.0.0");
released(&world.path("b/tb"), "tb", "1.0.0");
for name in ["a", "b"] {
let folder = world.path(name).display().to_string();
world.ok(&["catalog", "add", &folder, "--name", name]);
}
let out = world.lx(&["add", "b:tb"]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(out.status.success(), "{stderr}");
assert!(
stderr.contains("warning: `tb` is in b, a; b's is used"),
"{stderr}"
);
}
fn tagged(dir: &Path, tag: &str) {
if !dir.join(".git").exists() {
git(dir, &["init", "-q", "-b", "main"]);
}
git(dir, &["add", "-A"]);
git(dir, &["commit", "-q", "-m", tag]);
git(dir, &["tag", tag]);
}
fn append_manifest(dir: &Path, text: &str) {
let manifest = std::fs::read_to_string(dir.join("logoxide.toml")).unwrap();
std::fs::write(dir.join("logoxide.toml"), format!("{manifest}{text}")).unwrap();
}
fn installed_dir(world: &World, id: &str) -> PathBuf {
let mut versions = std::fs::read_dir(world.path(&format!("library/packages/{id}"))).unwrap();
versions.next().unwrap().unwrap().path()
}
#[test]
fn a_failed_add_or_remove_leaves_the_manifest_alone() {
let world = World::new("failed-add");
world.ok(&["init"]);
let manifest = world.path("vault/logoxide.toml");
let before = std::fs::read(&manifest).unwrap();
let missing = world.path("nothing").display().to_string();
assert_eq!(world.lx(&["add", "--git", &missing]).status.code(), Some(1));
assert_eq!(std::fs::read(&manifest).unwrap(), before);
let repo = world.path("tb");
package(&repo, "tb", "1.0.0");
append_manifest(
&repo,
&format!("\n[dependencies]\ngone = {{ git = \"{missing}\" }}\n"),
);
tagged(&repo, "v1.0.0");
let out = world.lx(&["add", "--git", &repo.display().to_string()]);
assert_eq!(out.status.code(), Some(1));
assert_eq!(std::fs::read(&manifest).unwrap(), before);
assert!(!world.path("vault/logoxide.lock").exists());
let other = world.path("ob");
released(&other, "ob", "1.0.0");
let kept = world.path("kb");
released(&kept, "kb", "1.0.0");
world.ok(&["add", "--git", &other.display().to_string()]);
world.ok(&["add", "--git", &kept.display().to_string()]);
let before = std::fs::read(&manifest).unwrap();
std::fs::write(installed_dir(&world, "kb").join("usx/JHN.usx"), "edited").unwrap();
assert_eq!(world.lx(&["remove", "ob"]).status.code(), Some(1));
assert_eq!(std::fs::read(&manifest).unwrap(), before);
}
fn head(dir: &Path) -> String {
let out = Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(dir)
.output()
.unwrap();
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
#[test]
fn revisions_are_whole_commits() {
let world = World::new("rev");
world.ok(&["init"]);
let repo = world.path("tb");
released(&repo, "tb", "1.0.0");
let commit = head(&repo);
let url = repo.display().to_string();
let out = world.lx(&["add", "--git", &url, "--rev", &commit[..8]]);
assert_eq!(out.status.code(), Some(1));
let error = String::from_utf8_lossy(&out.stderr);
assert!(error.contains("isn't a full commit id"), "{error}");
let added = world.ok(&["add", "--git", &url, "--rev", &commit]);
assert!(added.contains("added tb 1.0.0"), "{added}");
}
#[test]
fn update_names_only_what_is_there() {
let world = World::new("update");
world.ok(&["init"]);
let repo = world.path("tb");
released(&repo, "tb", "1.0.0");
world.ok(&["add", "--git", &repo.display().to_string()]);
let out = world.lx(&["update", "tbb"]);
assert_eq!(out.status.code(), Some(1));
let error = String::from_utf8_lossy(&out.stderr);
assert!(
error.contains("nothing here is named tbb (the manifest has tb)"),
"{error}"
);
world.ok(&["update", "tb"]);
world.ok(&["update"]);
}
#[test]
fn a_gone_commit_and_a_moved_tag_are_said() {
let world = World::new("moved");
world.ok(&["init"]);
let repo = world.path("tb");
released(&repo, "tb", "1.0.0");
world.ok(&["add", "--git", &repo.display().to_string()]);
std::fs::write(repo.join("usx/JHN.usx"), JHN.replace("loved", "LOVED")).unwrap();
git(
&repo,
&["commit", "-q", "-a", "--amend", "-m", "1.0.0 again"],
);
git(&repo, &["tag", "-f", "v1.0.0"]);
git(&repo, &["reflog", "expire", "--expire=now", "--all"]);
git(&repo, &["gc", "-q", "--prune=now"]);
std::fs::remove_dir_all(world.path("library/packages")).unwrap();
let out = world.lx(&["install"]);
assert_eq!(out.status.code(), Some(1));
let error = String::from_utf8_lossy(&out.stderr);
assert!(error.contains("may no longer be on the host"), "{error}");
assert!(error.contains("`lx update tb`"), "{error}");
let out = world.lx(&["update", "tb"]);
assert!(out.status.success());
let warned = String::from_utf8_lossy(&out.stderr);
assert!(
warned.contains("warning: tb 1.0.0: v1.0.0 was moved"),
"{warned}"
);
assert!(!String::from_utf8_lossy(&world.lx(&["update"]).stderr).contains("moved"));
}
#[test]
fn tags_that_are_not_releases() {
let world = World::new("tags");
world.ok(&["init"]);
let repo = world.path("tb");
package(&repo, "tb", "1.0.0");
tagged(&repo, "v1.0");
let url = repo.display().to_string();
let out = world.lx(&["add", "--git", &url]);
assert_eq!(out.status.code(), Some(1));
let error = String::from_utf8_lossy(&out.stderr);
assert!(error.contains("has no releases"), "{error}");
assert!(error.contains("v1.0 names no version"), "{error}");
assert!(error.contains("--branch or --rev"), "{error}");
let added = world.ok(&["add", "--git", &url, "--branch", "main"]);
assert!(added.contains("added tb 1.0.0"), "{added}");
world.ok(&["remove", "tb"]);
git(&repo, &["tag", "v1.0.0"]);
let out = world.lx(&["add", "--git", &url]);
assert!(out.status.success());
let warned = String::from_utf8_lossy(&out.stderr);
assert!(warned.contains("warning:"), "{warned}");
assert!(warned.contains("v1.0 names no version"), "{warned}");
}
#[test]
fn locked_and_offline_installs() {
let world = World::new("frozen");
world.ok(&["init"]);
let repo = world.path("tb");
released(&repo, "tb", "1.0.0");
world.ok(&["add", "--git", &repo.display().to_string()]);
let lock_path = world.path("vault/logoxide.lock");
let lock = std::fs::read(&lock_path).unwrap();
world.ok(&["install", "--locked"]);
world.ok(&["install", "--offline"]);
world.ok(&["install", "--locked", "--offline"]);
assert_eq!(std::fs::read(&lock_path).unwrap(), lock);
let other = world.path("ob");
released(&other, "ob", "1.0.0");
let manifest_path = world.path("vault/logoxide.toml");
let manifest = std::fs::read_to_string(&manifest_path).unwrap();
std::fs::write(
&manifest_path,
format!("{manifest}ob = {{ git = \"{}\" }}\n", other.display()),
)
.unwrap();
let out = world.lx(&["install", "--locked"]);
assert_eq!(out.status.code(), Some(1));
let error = String::from_utf8_lossy(&out.stderr);
assert!(error.contains("`ob` isn't locked"), "{error}");
assert!(error.contains("--locked"), "{error}");
let out = world.lx(&["install", "--offline"]);
assert_eq!(out.status.code(), Some(1));
let error = String::from_utf8_lossy(&out.stderr);
assert!(error.contains("offline nothing is fetched"), "{error}");
assert_eq!(std::fs::read(&lock_path).unwrap(), lock);
assert!(!world.path("library/packages/ob").exists());
std::fs::write(&manifest_path, manifest).unwrap();
std::fs::remove_dir_all(world.path("library/packages")).unwrap();
let out = world.lx(&["install", "--offline"]);
assert_eq!(out.status.code(), Some(1));
let error = String::from_utf8_lossy(&out.stderr);
assert!(
error.contains("tb 1.0.0 isn't installed, and offline nothing is fetched"),
"{error}"
);
world.ok(&["install", "--locked"]);
world.ok(&["install", "--offline"]);
assert_eq!(std::fs::read(&lock_path).unwrap(), lock);
std::fs::remove_file(&lock_path).unwrap();
assert_eq!(world.lx(&["install", "--locked"]).status.code(), Some(1));
assert!(!lock_path.exists());
}
#[test]
fn two_bibles_with_one_name_are_warned_of() {
let world = World::new("clash");
world.ok(&["init"]);
let repo = world.path("tb");
released(&repo, "tb", "1.0.0");
released(&repo, "tb", "2.0.0");
let shelf = world.path("shelf");
std::fs::create_dir_all(&shelf).unwrap();
std::fs::write(
shelf.join("logoxide.toml"),
format!(
"[package]\nid = \"shelf\"\nname = \"Shelf\"\nabbreviation = \"SH\"\nversion = \"1.0.0\"\nkind = \"collection\"\nlanguage = \"en\"\n\n[dependencies]\ntb = {{ git = \"{}\", version = \"1\" }}\n",
repo.display()
),
)
.unwrap();
tagged(&shelf, "v1.0.0");
world.ok(&["add", "--git", &repo.display().to_string()]);
world.ok(&["add", "--git", &shelf.display().to_string()]);
let out = world.lx(&["install"]);
assert!(out.status.success());
let warned = String::from_utf8_lossy(&out.stderr);
assert!(
warned.contains("warning: `TB` names tb 1.0.0 and tb 2.0.0"),
"{warned}"
);
world.ok(&["remove", "tb"]);
let added = world.lx(&[
"add",
"--git",
&repo.display().to_string(),
"--abbreviation",
"TB2",
]);
assert!(added.status.success());
let out = world.lx(&["install"]);
assert!(!String::from_utf8_lossy(&out.stderr).contains("warning"));
}
#[test]
fn diff_names_joined_and_split_verses() {
let world = World::new("diff-bridges");
let tb = world.path("tb");
released(&tb, "tb", "1.0.0");
let path = tb.to_str().unwrap();
let joined = JHN.replace(
r#"<verse number="16" style="v"/>For God so loved <verse number="17" style="v"/>"#,
r#"<verse number="16-17" style="v"/>For God so loved "#,
);
std::fs::write(tb.join("usx/JHN.usx"), &joined).unwrap();
let d = world.json(&["--format", "json", "diff", "--path", path, "v1.0.0"]);
assert_eq!(d["change"], "major");
assert_eq!(
d["joined"],
serde_json::json!([["JHN 3:17", "JHN 3:16-17"]])
);
assert_eq!(d["removed"], serde_json::json!([]));
let text = world.ok(&["diff", "--path", path, "v1.0.0"]);
assert!(
text.contains("JHN 3:17 is joined into JHN 3:16-17"),
"{text}"
);
git(&tb, &["commit", "-qam", "joined"]);
git(&tb, &["tag", "v2.0.0"]);
std::fs::write(tb.join("usx/JHN.usx"), JHN).unwrap();
let d = world.json(&["--format", "json", "diff", "--path", path, "v2.0.0"]);
assert_eq!(d["change"], "minor");
assert_eq!(d["split"], serde_json::json!(["JHN 3:16"]));
assert_eq!(d["added"], serde_json::json!([]));
}
#[test]
fn two_editions_of_one_work_are_said() {
let world = World::new("works");
for (id, work) in [("kjv1769", "kjv"), ("kjv1611", "kjv")] {
let dir = world.path(id);
package(&dir, id, "1.0.0");
let manifest = std::fs::read_to_string(dir.join("logoxide.toml")).unwrap();
let manifest = manifest.replace(
"license = \"CC0-1.0\"",
&format!("license = \"CC0-1.0\"\nwork = \"{work}\"\nedition = \"{id}\""),
);
std::fs::write(dir.join("logoxide.toml"), manifest).unwrap();
}
world.ok(&["init"]);
let out = world.lx(&["add", "--path", "../kjv1769"]);
assert!(!String::from_utf8_lossy(&out.stderr).contains("are all"));
let out = world.lx(&["add", "--path", "../kjv1611"]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(out.status.success(), "{stderr}");
assert!(
stderr.contains("kjv1769 1.0.0") && stderr.contains("are all King James Version"),
"{stderr}"
);
let info = world.ok(&["info", "kjv1611"]);
assert!(
info.contains("work: King James Version (`kjv`), edition kjv1611"),
"{info}"
);
}
#[test]
fn a_recording_is_checked_against_the_text_it_reads() {
let world = World::new("recording");
package(&world.path("bsb"), "bsb", "1.0.0");
let recording = world.path("bsb-audio");
std::fs::create_dir_all(recording.join("timings")).unwrap();
let sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
std::fs::write(
recording.join("logoxide.toml"),
format!(
"[package]\nid = \"bsb-audio\"\nname = \"BSB read aloud\"\nabbreviation = \"BSBA\"\n\
version = \"1.0.0\"\nlanguage = \"en\"\n\n[dependencies]\nbsb = {{ path = \"../bsb\" }}\n\n\
[media]\ntext = \"bsb\"\ntimings = \"timings/\"\n\n[[media.files]]\npath = \"JHN.mp3\"\n\
url = \"https://example.org/JHN.mp3\"\nsize = 0\nsha256 = \"{sha256}\"\nbook = \"JHN\"\n"
),
)
.unwrap();
std::fs::write(
recording.join("timings/JHN.vtt"),
"WEBVTT\n\nJHN 3:16\n00:00.000 --> 00:04.000\n\nJHN 3:18\n00:04.000 --> 00:08.000\n",
)
.unwrap();
let path = recording.to_str().unwrap();
let out = world.lx(&["media", "check", "--offline", path]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("[media] text `bsb`"), "{stderr}");
world.ok(&["--project", path, "install"]);
let out = world.lx(&["check", path]);
let text = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(!out.status.success(), "{text}");
assert!(
text.contains("JHN 3:18: the text has no verse 18") && text.contains("a recording of bsb"),
"{text}"
);
}
#[test]
fn publishing_what_isnt_redistributable_is_confirmed() {
let world = World::new("publish-rights");
let dir = world.path("not-mine");
package(&dir, "not-mine", "1.0.0");
append_manifest(&dir, "\n[package.rights]\nredistributable = false\n");
tagged(&dir, "v1.0.0");
let registry = world.path("registry");
world.ok(&["registry", "init", registry.to_str().unwrap()]);
let publish = [
"publish",
dir.to_str().unwrap(),
"--to",
registry.to_str().unwrap(),
];
let out = world.lx(&publish);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(!out.status.success(), "{stderr}");
assert!(
stderr.contains("WARNING: not-mine's rights say it isn't redistributable")
&& stderr.contains("--yes"),
"{stderr}"
);
let out = world.lx(&[&publish[..], &["--yes"]].concat());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(out.status.success(), "{stderr}");
assert!(stderr.contains("isn't redistributable"), "{stderr}");
let catalog = std::fs::read_to_string(registry.join("catalog.toml")).unwrap();
assert!(
catalog.contains("rights = { redistributable = false }"),
"{catalog}"
);
}
#[test]
fn a_held_library_or_project_is_busy() {
let world = World::new("busy");
released(&world.path("kjv"), "kjv", "1.0.0");
world.ok(&["init"]);
world.ok(&["add", "--path", "../kjv"]);
let library = world.path("library");
std::fs::create_dir_all(&library).unwrap();
let held =
logoxide::locking::lock_file(&world.lock_path(&library), "the library", LOCK_WAIT).unwrap();
let out = world.lx(&["install"]);
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("the library at") && stderr.contains("in use by another process ("),
"{stderr}"
);
drop(held);
world.ok(&["install"]);
let held = logoxide::locking::lock_file(
&world.lock_path(&world.path("vault")),
"the project",
LOCK_WAIT,
)
.unwrap();
let out = world.lx(&["disable", "kjv"]);
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("the project at") && stderr.contains("in use"),
"{stderr}"
);
drop(held);
world.ok(&["disable", "kjv"]);
let stale = world.path("library/packages/kjv/.1.0.0.staging-1-0-deadbeef");
std::fs::create_dir_all(&stale).unwrap();
let out = world.ok(&["clean", "--remove"]);
assert!(!out.contains("staging"), "{out}");
assert!(stale.is_dir());
let old = std::time::SystemTime::now() - std::time::Duration::from_secs(2 * 24 * 60 * 60);
std::fs::File::open(&stale)
.unwrap()
.set_modified(old)
.unwrap();
let out = world.ok(&["clean"]);
assert!(out.contains("stale:") && stale.is_dir(), "{out}");
let out = world.ok(&["clean", "--remove"]);
assert!(
out.contains("left by an install that didn't finish"),
"{out}"
);
assert!(!stale.exists());
}
const LOCK_WAIT: std::time::Duration = std::time::Duration::from_millis(100);
#[test]
fn clean_keeps_what_other_projects_locked_until_their_roots_are_pruned() {
let world = World::new("roots");
released(&world.path("tb"), "tb", "1.0.0");
world.ok(&["init"]);
let other = world.path("other");
world.ok(&["init", "../other"]);
let other_s = other.display().to_string();
let tb = world.path("tb").display().to_string();
world.ok(&["--project", &other_s, "add", "--git", &tb]);
assert_eq!(
std::fs::read_dir(world.path("library/roots"))
.unwrap()
.count(),
1
);
let out = world.ok(&["clean"]);
assert!(out.contains("every installed version is used"), "{out}");
std::fs::remove_dir_all(&other).unwrap();
let out = world.ok(&["clean"]);
assert!(
out.contains("its project is gone, but its 1 version stay")
&& out.contains("every installed version is used"),
"{out}"
);
let out = world.ok(&["clean", "--prune-roots"]);
assert!(
out.contains("forgot") && out.contains("unused: tb 1.0.0+g"),
"{out}"
);
assert!(
world
.ok(&["clean", "--remove"])
.contains("removed tb 1.0.0+g")
);
std::fs::write(world.path("library/roots/bad.toml"), "path = 1\n").unwrap();
let out = world.lx(&["clean"]);
assert!(!out.status.success());
assert!(String::from_utf8_lossy(&out.stderr).contains("bad.toml"));
}
fn repository(dir: &Path) {
git(dir, &["init", "-q", "-b", "main"]);
git(dir, &["config", "user.name", "t"]);
git(dir, &["config", "user.email", "t@t"]);
git(dir, &["config", "commit.gpgsign", "false"]);
git(dir, &["config", "tag.gpgsign", "false"]);
}
fn dictionary(dir: &Path, version: &str, g25: &str) {
std::fs::create_dir_all(dir.join("entries")).unwrap();
std::fs::write(
dir.join("logoxide.toml"),
format!("[package]\nid = \"strongs\"\nname = \"Strong's Greek\"\nabbreviation = \"STR\"\nversion = \"{version}\"\nlanguage = \"grc\"\nlicense = \"CC0-1.0\"\nkind = \"lexicon\"\nkeys = \"strongs-greek\"\n\n[formats]\ndictionary = {{ path = \"entries/\" }}\n"),
)
.unwrap();
std::fs::write(
dir.join("entries/greek.xml"),
format!("<entries keys=\"strongs-greek\" markup=\"tei\">\n<entry key=\"G0025\" headword=\"ἀγαπάω\"><entryFree><orth>ἀγαπάω</orth> {g25}</entryFree></entry>\n<entry key=\"G26\">love</entry>\n</entries>\n"),
)
.unwrap();
}
fn xref(dir: &Path, version: &str, lines: &str) {
std::fs::create_dir_all(dir.join("xref")).unwrap();
std::fs::write(
dir.join("logoxide.toml"),
format!("[package]\nid = \"tsk\"\nname = \"Treasury\"\nabbreviation = \"TSK\"\nversion = \"{version}\"\nlanguage = \"en\"\nlicense = \"CC0-1.0\"\nkind = \"cross-references\"\n\n[formats]\nxref = {{ path = \"xref/\" }}\n"),
)
.unwrap();
std::fs::write(dir.join("xref/gen.tsv"), lines).unwrap();
}
fn commentary(dir: &Path) {
std::fs::create_dir_all(dir.join("osis")).unwrap();
std::fs::write(
dir.join("logoxide.toml"),
"[package]\nid = \"mhcc\"\nname = \"A Commentary\"\nabbreviation = \"MHCC\"\nversion = \"1.0.0\"\nlanguage = \"en\"\nlicense = \"CC0-1.0\"\nkind = \"commentary\"\n\n[formats]\nosis = { path = \"osis/\" }\n",
)
.unwrap();
std::fs::write(
dir.join("osis/John.xml"),
r#"<osis><osisText osisIDWork="MHCC"><div type="book" osisID="John"><chapter osisID="John.3" sID="John.3"/><verse osisID="John.3.16" sID="John.3.16" n="16"/>God so loved: the <hi type="italic">world</hi>.<verse eID="John.3.16"/><verse osisID="John.3.17 John.3.18" sID="John.3.17" n="17-18"/>Not to condemn.<verse eID="John.3.17"/><chapter eID="John.3"/></div></osisText></osis>"#,
)
.unwrap();
}
#[test]
fn a_dictionarys_entries_are_shown_released_and_verified() {
let world = World::new("dictionary");
let dir = world.path("strongs");
dictionary(&dir, "1.0.0", "to love");
let path = dir.display().to_string();
assert!(world.ok(&["check", &path]).contains("2 entries"));
assert_eq!(
world.json(&["check", &path, "--format", "json"])["entries"],
2
);
let shown = world.ok(&["show", &path, "--entry", "g0025"]);
assert_eq!(shown, "G25 ἀγαπάω\nἀγαπάω to love\n");
let entry = world.json(&["--format", "json", "show", &path, "--entry", "G25"]);
assert_eq!(entry["key"], "G25");
assert_eq!(entry["headword"], "ἀγαπάω");
assert_eq!(entry["text"], "ἀγαπάω to love");
assert_eq!(world.ok(&["show", &path, "--entry", "G26"]), "G26\nlove\n");
assert_eq!(world.ok(&["show", &path, "--keys"]), "G25\nG26\n");
assert_eq!(
world.ok(&["show", &path, "--keys", "--limit", "1"]),
"G25\n"
);
assert_eq!(
world.json(&["--format", "json", "show", &path, "--keys"]),
serde_json::json!(["G25", "G26"])
);
let out = world.lx(&["show", &path, "--entry", "G27"]);
assert_eq!(out.status.code(), Some(1));
assert!(String::from_utf8_lossy(&out.stderr).contains("no entry `G27`"));
assert_eq!(
world.json(&["--format", "json", "show", &path, "--entry", "G27"]),
serde_json::Value::Null
);
repository(&dir);
assert!(world.ok(&["release", &path]).contains("released v1.0.0"));
assert!(dir.join("logoxide.entries.bin").is_file());
assert!(world.ok(&["verify", &path]).contains("matches"));
dictionary(&dir, "1.0.0", "to love, cherish");
let out = world.lx(&["verify", &path]);
assert_eq!(out.status.code(), Some(1));
assert!(String::from_utf8_lossy(&out.stderr).contains("doesn't match"));
let d = world.json(&["--format", "json", "diff", "--path", &path, "v1.0.0"]);
assert_eq!(d["change"], "minor");
assert_eq!(d["description"], "1 entry reworded");
}
#[test]
fn cross_references_are_shown_released_and_checked() {
let world = World::new("xref");
let dir = world.path("tsk");
xref(
&dir,
"1.0.0",
"# Genesis\nGen.1.1\tJohn.1.1-John.1.3\t1\nGen.1.2\tHeb.11.3\n",
);
let path = dir.display().to_string();
assert!(world.ok(&["check", &path]).contains("2 entries"));
assert_eq!(
world.ok(&["show", &path, "--xref", "Genesis 1:1"]),
"Gen.1.1\tJohn.1.1-John.1.3\t1\n"
);
assert_eq!(
world.json(&["--format", "json", "show", &path, "--xref", "Gen 1:1-2"]),
serde_json::json!([
{ "from": "Gen.1.1", "to": "John.1.1-John.1.3", "weight": 1.0 },
{ "from": "Gen.1.2", "to": "Heb.11.3", "weight": null }
])
);
assert!(
world
.ok(&["show", &path, "--xref", "Genesis 2:1"])
.contains("no cross-references")
);
let out = world.lx(&["show", &path, "--xref", "nowhere 1:1"]);
assert_eq!(out.status.code(), Some(1));
assert_eq!(world.ok(&["show", &path, "--keys"]), "Gen.1.1\nGen.1.2\n");
repository(&dir);
assert!(world.ok(&["release", &path]).contains("released v1.0.0"));
assert!(world.ok(&["verify", &path]).contains("matches"));
xref(&dir, "1.0.0", "Gen.1.1\tJohn.1.1\nGen.1.2\tNowhere.1.1\n");
let out = world.lx(&["check", &path]);
assert_eq!(out.status.code(), Some(1));
let said = String::from_utf8_lossy(&out.stderr);
assert!(said.contains("xref/gen.tsv:2"), "{said}");
}
#[test]
fn a_commentarys_notes_are_shown() {
let world = World::new("commentary");
let dir = world.path("mhcc");
commentary(&dir);
let path = dir.display().to_string();
assert!(world.ok(&["check", &path]).contains("1 books, 2 notes"));
assert_eq!(
world.ok(&["show", &path, "--notes", "John 3:17"]),
"John 3:17-18\nNot to condemn.\n"
);
assert_eq!(
world.ok(&["show", &path, "--notes", "John 3:16-18"]),
"John 3:16\nGod so loved: the world.\n\nJohn 3:17-18\nNot to condemn.\n"
);
let notes = world.json(&["--format", "json", "show", &path, "--notes", "John 3:16"]);
assert_eq!(
notes,
serde_json::json!([{
"reference": "John 3:16",
"chapter": 3, "verse": 16, "last": 16, "lastChapter": 3, "part": 0,
"text": "God so loved: the world."
}])
);
assert!(
world
.ok(&["show", &path, "--notes", "Romans 1:1"])
.contains("no notes")
);
assert_eq!(
world.json(&["--format", "json", "show", &path, "--notes", "John 4:1"]),
serde_json::json!([])
);
assert!(
world
.ok(&["show", &path, "--notes", "John 3:19"])
.contains("no notes")
);
assert_eq!(
world.ok(&["show", &path, "--notes", "John 3:19", "--stretch"]),
"John 3:17-18\nNot to condemn.\n"
);
assert_eq!(
world.lx(&["show", &path, "--stretch"]).status.code(),
Some(2)
);
let out = world.lx(&["show", &path, "--notes", "John 3:16", "--keys"]);
assert_eq!(out.status.code(), Some(2));
}
fn numbered_bible(dir: &Path, id: &str, versification: &str, sixteen: &str, seventeen: &str) {
std::fs::create_dir_all(dir.join("usx")).unwrap();
std::fs::write(
dir.join("logoxide.toml"),
format!(
"[package]\nid = \"{id}\"\nname = \"Test {id}\"\nabbreviation = \"{}\"\nversion = \"1.0.0\"\nlanguage = \"en\"\nlicense = \"CC0-1.0\"\nversification = \"{versification}\"\n\n[formats]\nusx = {{ path = \"usx/\" }}\n",
id.to_uppercase()
),
)
.unwrap();
std::fs::write(
dir.join("usx/JHN.usx"),
format!(
r#"<usx version="3.0"><book code="JHN" style="id"/><chapter number="3" style="c"/><para style="p"><verse number="16" style="v"/>{sixteen} <verse number="17" style="v"/>{seventeen}</para></usx>"#
),
)
.unwrap();
}
#[test]
fn a_passage_is_read_from_the_bible_it_names() {
let world = World::new("read");
package(&world.path("one"), "one", "1.0.0");
package(&world.path("two"), "two", "1.0.0");
std::fs::write(
world.path("two/usx/JHN.usx"),
r#"<usx version="3.0"><book code="JHN" style="id"/><chapter number="3" style="c"/><para style="p"><verse number="16" style="v"/>God loved the world. <verse number="17" style="v"/>He sent his Son.</para></usx>"#,
)
.unwrap();
world.ok(&["init"]);
world.ok(&["add", "--path", "../one"]);
world.ok(&["add", "--path", "../two"]);
assert_eq!(
world.ok(&["read", "John 3:16 two"]),
"John 3:16 (TWO)\n[16] God loved the world.\n"
);
assert_eq!(
world.ok(&["read", "John 3:16-17", "--no-labels"]),
"John 3:16-17 (ONE)\nFor God so loved For God sent\n"
);
let read = world.json(&["--format", "json", "read", "John 3:17 (TWO)"]);
assert_eq!(
read,
serde_json::json!({
"bible": "two", "abbreviation": "TWO", "reference": "John 3:17",
"osis": "John.3.17", "versification": "own", "text": "[17] He sent his Son."
})
);
let out = world.lx(&["read", "John 3:16 three"]);
assert_eq!(out.status.code(), Some(1));
assert!(String::from_utf8_lossy(&out.stderr).contains("no Bible `three`"));
let out = world.lx(&["read", "John 3:18 ONE"]);
assert_eq!(out.status.code(), Some(1));
assert!(String::from_utf8_lossy(&out.stderr).contains("isn't a Bible reference"));
assert_eq!(
world.ok(&["compare", "John 3:16", "--bibles", "TWO,one"]),
"John 3:16 (TWO)\n[16] God loved the world.\n\nJohn 3:16 (ONE)\n[16] For God so loved\n"
);
let all = world.json(&["--format", "json", "compare", "John 3:17"]);
assert_eq!(all.as_array().map(Vec::len), Some(2));
assert_eq!(all[1]["text"], "[17] He sent his Son.");
let out = world.lx(&["compare", "John 3:16", "--bibles", "three"]);
assert_eq!(out.status.code(), Some(1));
assert!(String::from_utf8_lossy(&out.stderr).contains("no Bible `three`"));
}
#[test]
fn a_passage_is_compared_in_each_bibles_numbering() {
let world = World::new("compare");
numbered_bible(
&world.path("eng"),
"eng",
"English",
"For God so loved",
"For God sent",
);
numbered_bible(
&world.path("shi"),
"shi",
"Shifted",
"Before it.",
"God loved the world.",
);
let map = world.path("map");
std::fs::create_dir_all(&map).unwrap();
std::fs::write(
map.join("logoxide.toml"),
"[package]\nid = \"eng-shi\"\nname = \"English to Shifted\"\nabbreviation = \"ENG-SHI\"\nversion = \"1.0.0\"\nkind = \"versification-map\"\nlanguage = \"en\"\nlicense = \"CC0-1.0\"\nversification = \"English\"\nmaps-to = \"Shifted\"\n\n[formats]\nvrs = { path = \"eng-shi.vrs\" }\n",
)
.unwrap();
std::fs::write(map.join("eng-shi.vrs"), "JHN 3:16 = JHN 3:17\n").unwrap();
world.ok(&["init"]);
for dir in ["../eng", "../shi", "../map"] {
world.ok(&["add", "--path", dir]);
}
assert_eq!(
world.ok(&["compare", "John 3:16 ENG", "--bibles", "ENG,SHI"]),
"John 3:16 (ENG)\n[16] For God so loved\n\nJohn 3:17 (SHI, numbered Shifted)\n[17] God loved the world.\n"
);
let compared = world.json(&[
"--format",
"json",
"compare",
"John 3:16-17",
"--bibles",
"ENG,SHI",
]);
assert_eq!(compared[1]["reference"], "John 3:17");
assert_eq!(compared[1]["mapped"], true);
assert_eq!(compared[1]["missing"], "John 3:17");
assert_eq!(compared[0]["missing"], "");
}
struct Served {
url: String,
body: std::sync::Arc<std::sync::Mutex<Option<String>>>,
asked: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
impl Served {
fn new(body: &str) -> Served {
use std::io::{BufRead, Write};
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let url = format!(
"http://127.0.0.1:{}/catalog.toml",
listener.local_addr().unwrap().port()
);
let body = std::sync::Arc::new(std::sync::Mutex::new(Some(body.to_string())));
let asked = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let (serving, counting) = (body.clone(), asked.clone());
std::thread::spawn(move || {
for stream in listener.incoming() {
let Ok(mut stream) = stream else { continue };
let mut reader = std::io::BufReader::new(stream.try_clone().unwrap());
let mut line = String::new();
while reader.read_line(&mut line).is_ok_and(|n| n > 0) && line != "\r\n" {
line.clear();
}
counting.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let answer = match serving.lock().unwrap().clone() {
Some(body) => format!(
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
),
None => String::from(
"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
),
};
let _ = stream.write_all(answer.as_bytes());
}
});
Served { url, body, asked }
}
fn asked(&self) -> usize {
self.asked.load(std::sync::atomic::Ordering::SeqCst)
}
fn set(&self, body: Option<&str>) {
*self.body.lock().unwrap() = body.map(str::to_string);
}
}
fn served_catalog(id: &str) -> String {
format!(
"[[package]]\nid = \"{id}\"\nname = \"Package {id}\"\nabbreviation = \"{}\"\nlanguage = \"en\"\nlicense = \"CC0-1.0\"\ngit = \"https://example.org/{id}\"\n",
id.to_uppercase()
)
}
#[test]
fn search_keeps_catalogs_downloaded_until_theyre_old() {
let world = World::new("catalog-cache");
let served = Served::new(&served_catalog("served"));
std::fs::create_dir_all(world.path("vault")).unwrap();
std::fs::write(
world.path("vault/logoxide.toml"),
format!(
"[settings]\ndefault-catalogs = false\ncatalogs = [\"{}\", \"local.toml\"]\n",
served.url
),
)
.unwrap();
std::fs::write(world.path("vault/local.toml"), served_catalog("local")).unwrap();
let ids = |found: &serde_json::Value| -> Vec<String> {
(found.as_array().unwrap().iter())
.map(|e| e["id"].as_str().unwrap().to_string())
.collect()
};
let found = world.json(&["search", "served", "--json"]);
assert_eq!(ids(&found), ["served"]);
assert_eq!(served.asked(), 1);
world.json(&["search", "served", "--json"]);
assert_eq!(served.asked(), 1);
served.set(Some(&served_catalog("newer")));
let mut offline = ids(&world.json(&["search", "--offline", "--json"]));
offline.sort();
assert_eq!(offline, ["local", "served"]);
assert_eq!(served.asked(), 1);
assert_eq!(
ids(&world.json(&["search", "newer", "--refresh", "--json"])),
["newer"]
);
assert_eq!(served.asked(), 2);
served.set(Some(&served_catalog("newest")));
let out = world.ok(&["catalog", "refresh"]);
assert!(
out.contains(&format!("Refreshed {}: 1 package", served.url)),
"{out}"
);
assert!(!out.contains("local.toml"), "{out}");
assert_eq!(served.asked(), 3);
assert_eq!(
ids(&world.json(&["search", "newest", "--json"])),
["newest"]
);
assert_eq!(served.asked(), 3);
served.set(None);
let out = world.lx(&["search", "newest", "--refresh"]);
assert!(out.status.success());
let said = String::from_utf8_lossy(&out.stderr);
assert!(said.contains("the copy downloaded"), "{said}");
assert!(String::from_utf8_lossy(&out.stdout).contains("newest"));
let report = world.lx(&["catalog", "refresh", &served.url, "--format", "json"]);
assert!(!report.status.success());
let report: serde_json::Value = serde_json::from_slice(&report.stdout).unwrap();
assert_eq!(report[0]["url"], served.url.as_str());
assert_eq!(report[0]["refreshed"], false);
assert!(
world
.lx(&["catalog", "refresh", "nothing-here"])
.status
.code()
!= Some(0)
);
served.set(Some(&served_catalog("served")));
std::fs::write(world.path("config.toml"), "catalog-max-age = 0\n").unwrap();
let asked = served.asked();
world.json(&["search", "served", "--json"]);
world.json(&["search", "served", "--json"]);
assert_eq!(served.asked(), asked + 2);
std::fs::write(world.path("config.toml"), "catalog-max-age = \"soon\"\n").unwrap();
assert!(!world.lx(&["search", "served"]).status.success());
}
#[test]
fn catalog_refresh_with_nothing_downloaded() {
let world = World::new("catalog-refresh-local");
std::fs::create_dir_all(world.path("vault")).unwrap();
std::fs::write(
world.path("vault/logoxide.toml"),
"[settings]\ndefault-catalogs = false\ncatalogs = [\"local.toml\"]\n",
)
.unwrap();
std::fs::write(world.path("vault/local.toml"), served_catalog("local")).unwrap();
let out = world.ok(&["catalog", "refresh"]);
assert!(out.contains("nothing to refresh"), "{out}");
assert_eq!(
world.json(&["catalog", "refresh", "--format", "json"]),
serde_json::json!([])
);
}
#[test]
fn works_are_listed_with_registries_own() {
let world = World::new("works-listed");
world.ok(&["init"]);
let gill = world.path("reg/gill");
released(&gill, "gill", "1.0.0");
let manifest = std::fs::read_to_string(gill.join("logoxide.toml")).unwrap();
std::fs::write(
gill.join("logoxide.toml"),
manifest
.replace("version = \"1.0.0\"", "version = \"1.1.0\"")
.replace("language = \"en\"", "language = \"en\"\nwork = \"gill\""),
)
.unwrap();
git(&gill, &["commit", "-q", "-am", "1.1.0"]);
git(&gill, &["tag", "v1.1.0"]);
std::fs::write(
world.path("reg/works.toml"),
"[gill]\nname = \"John Gill's Exposition\"\naliases = [\"Gill\"]\nwikidata = \"Q1\"\n\n[kjv]\nname = \"Not the KJV\"\n",
)
.unwrap();
let folder = world.path("reg").display().to_string();
world.ok(&["catalog", "add", &folder, "--name", "reg"]);
let out = world.lx(&["works", "--registry", "reg", "--packages"]);
let (stdout, stderr) = (
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
assert!(out.status.success(), "{stderr}");
assert!(
stdout.contains("gill: John Gill's Exposition (named by reg), 1 package")
&& stdout.contains("GILL (gill): Test gill")
&& stdout.contains("kjv: King James Version (logoxide's table)"),
"{stdout}"
);
assert!(
stderr.contains("work `kjv`: logoxide's table names it"),
"{stderr}"
);
let works = world.json(&["works", "gill", "--registry", "reg", "--format", "json"]);
assert_eq!(works.as_array().unwrap().len(), 1, "{works}");
assert_eq!(works[0]["from"], "catalog");
assert_eq!(works[0]["catalog"], "reg");
assert_eq!(works[0]["wikidata"], "Q1");
assert_eq!(works[0]["packages"][0]["id"], "gill");
let found = world.json(&["search", "exposition", "--registry", "reg", "--json"]);
assert_eq!(found[0]["id"], "gill", "{found}");
let out = world.ok(&["catalog", "build", &folder]);
assert!(out.contains("1 work named"), "{out}");
let built = std::fs::read_to_string(world.path("reg/catalog.toml")).unwrap();
assert!(built.contains("[works.gill]"), "{built}");
}
#[test]
fn books_convert_between_usfm_usx_and_usj() {
let world = World::new("convert");
let usx = world.path("usx");
std::fs::create_dir_all(&usx).unwrap();
std::fs::write(usx.join("JHN.usx"), JHN).unwrap();
std::fs::write(usx.join("notes.txt"), "not a book").unwrap();
let usx = usx.to_string_lossy().to_string();
let usj = world.path("usj").to_string_lossy().to_string();
let said = world.ok(&["convert", &usx, "--to", "usj", "--output", &usj]);
assert!(said.contains("1 book written in USJ"), "{said}");
let json: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(world.path("usj/JHN.json")).unwrap())
.unwrap();
assert_eq!(json["type"], "USJ");
assert_eq!(json["content"][0]["code"], "JHN");
let book = world.path("book.txt");
std::fs::copy(world.path("usj/JHN.json"), &book).unwrap();
let usfm = world.ok(&["convert", &book.to_string_lossy(), "--to", "usfm"]);
assert_eq!(
usfm,
"\\id JHN\n\\c 3\n\\p \\v 16 For God so loved \\v 17 For God sent\n"
);
assert!(
!world
.lx(&["convert", &usx, "--to", "usfm"])
.status
.success()
);
let notes = world.path("usx/notes.txt").to_string_lossy().to_string();
let out = world.lx(&["convert", ¬es, "--to", "usj"]);
assert!(String::from_utf8_lossy(&out.stderr).contains("--from"));
}