mirror of
https://github.com/PR0M3TH3AN/Marlin.git
synced 2025-09-08 23:28:44 +00:00
update
This commit is contained in:
122
cli-bin/tests/e2e.rs
Normal file
122
cli-bin/tests/e2e.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
//! tests e2e.rs
|
||||
//! End-to-end “happy path” smoke-tests for the `marlin` binary.
|
||||
//!
|
||||
//! Run with `cargo test --test e2e` (CI does) or `cargo test`.
|
||||
|
||||
use assert_cmd::prelude::*;
|
||||
use predicates::prelude::*;
|
||||
use std::{fs, path::PathBuf, process::Command};
|
||||
use tempfile::tempdir;
|
||||
|
||||
/// Absolute path to the freshly-built `marlin` binary.
|
||||
fn marlin_bin() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_BIN_EXE_marlin"))
|
||||
}
|
||||
|
||||
/// Create the demo directory structure and seed files.
|
||||
fn spawn_demo_tree(root: &PathBuf) {
|
||||
fs::create_dir_all(root.join("Projects/Alpha")).unwrap();
|
||||
fs::create_dir_all(root.join("Projects/Beta")).unwrap();
|
||||
fs::create_dir_all(root.join("Projects/Gamma")).unwrap();
|
||||
fs::create_dir_all(root.join("Logs")).unwrap();
|
||||
fs::create_dir_all(root.join("Reports")).unwrap();
|
||||
|
||||
fs::write(root.join("Projects/Alpha/draft1.md"), "- [ ] TODO foo\n").unwrap();
|
||||
fs::write(root.join("Projects/Alpha/draft2.md"), "- [x] TODO foo\n").unwrap();
|
||||
fs::write(root.join("Projects/Beta/final.md"), "done\n").unwrap();
|
||||
fs::write(root.join("Projects/Gamma/TODO.txt"), "TODO bar\n").unwrap();
|
||||
fs::write(root.join("Logs/app.log"), "ERROR omg\n").unwrap();
|
||||
fs::write(root.join("Reports/Q1.pdf"), "PDF\n").unwrap();
|
||||
}
|
||||
|
||||
/// Shorthand for “run and must succeed”.
|
||||
fn ok(cmd: &mut Command) -> assert_cmd::assert::Assert {
|
||||
cmd.assert().success()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_cli_flow() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/* ── 1 ░ sandbox ───────────────────────────────────────────── */
|
||||
|
||||
let tmp = tempdir()?; // wiped on drop
|
||||
let demo_dir = tmp.path().join("marlin_demo");
|
||||
spawn_demo_tree(&demo_dir);
|
||||
|
||||
let db_path = demo_dir.join("index.db");
|
||||
|
||||
// Helper to spawn a fresh `marlin` Command with the DB env-var set
|
||||
let marlin = || {
|
||||
let mut c = Command::new(marlin_bin());
|
||||
c.env("MARLIN_DB_PATH", &db_path);
|
||||
c
|
||||
};
|
||||
|
||||
/* ── 2 ░ init ( auto-scan cwd ) ───────────────────────────── */
|
||||
|
||||
ok(marlin()
|
||||
.current_dir(&demo_dir)
|
||||
.arg("init"));
|
||||
|
||||
/* ── 3 ░ tag & attr demos ─────────────────────────────────── */
|
||||
|
||||
ok(marlin()
|
||||
.arg("tag")
|
||||
.arg(format!("{}/Projects/**/*.md", demo_dir.display()))
|
||||
.arg("project/md"));
|
||||
|
||||
ok(marlin()
|
||||
.arg("attr")
|
||||
.arg("set")
|
||||
.arg(format!("{}/Reports/*.pdf", demo_dir.display()))
|
||||
.arg("reviewed")
|
||||
.arg("yes"));
|
||||
|
||||
/* ── 4 ░ quick search sanity checks ───────────────────────── */
|
||||
|
||||
marlin()
|
||||
.arg("search").arg("TODO")
|
||||
.assert()
|
||||
.stdout(predicate::str::contains("TODO.txt"));
|
||||
|
||||
marlin()
|
||||
.arg("search").arg("attr:reviewed=yes")
|
||||
.assert()
|
||||
.stdout(predicate::str::contains("Q1.pdf"));
|
||||
|
||||
/* ── 5 ░ link flow & backlinks ────────────────────────────── */
|
||||
|
||||
let foo = demo_dir.join("foo.txt");
|
||||
let bar = demo_dir.join("bar.txt");
|
||||
fs::write(&foo, "")?;
|
||||
fs::write(&bar, "")?;
|
||||
|
||||
ok(marlin().arg("scan").arg(&demo_dir));
|
||||
|
||||
ok(marlin()
|
||||
.arg("link").arg("add")
|
||||
.arg(&foo).arg(&bar));
|
||||
|
||||
marlin()
|
||||
.arg("link").arg("backlinks").arg(&bar)
|
||||
.assert()
|
||||
.stdout(predicate::str::contains("foo.txt"));
|
||||
|
||||
/* ── 6 ░ backup → delete DB → restore ────────────────────── */
|
||||
|
||||
let backup_path = String::from_utf8(
|
||||
marlin().arg("backup").output()?.stdout
|
||||
)?;
|
||||
let backup_file = backup_path.split_whitespace().last().unwrap();
|
||||
|
||||
fs::remove_file(&db_path)?; // simulate corruption
|
||||
ok(marlin().arg("restore").arg(backup_file)); // restore
|
||||
|
||||
// Search must still work afterwards
|
||||
marlin()
|
||||
.arg("search").arg("TODO")
|
||||
.assert()
|
||||
.stdout(predicate::str::contains("TODO.txt"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
82
cli-bin/tests/neg.rs
Normal file
82
cli-bin/tests/neg.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
//! tests neg.rs
|
||||
//! Negative-path integration tests (“should fail / warn”).
|
||||
|
||||
use predicates::str;
|
||||
use tempfile::tempdir;
|
||||
|
||||
mod util;
|
||||
use util::marlin;
|
||||
|
||||
/* ───────────────────────── LINKS ─────────────────────────────── */
|
||||
|
||||
#[test]
|
||||
fn link_non_indexed_should_fail() {
|
||||
let tmp = tempdir().unwrap();
|
||||
|
||||
marlin(&tmp).current_dir(tmp.path()).arg("init").assert().success();
|
||||
|
||||
std::fs::write(tmp.path().join("foo.txt"), "").unwrap();
|
||||
std::fs::write(tmp.path().join("bar.txt"), "").unwrap();
|
||||
|
||||
marlin(&tmp)
|
||||
.current_dir(tmp.path())
|
||||
.args([
|
||||
"link", "add",
|
||||
&tmp.path().join("foo.txt").to_string_lossy(),
|
||||
&tmp.path().join("bar.txt").to_string_lossy()
|
||||
])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(str::contains("file not indexed"));
|
||||
}
|
||||
|
||||
/* ───────────────────────── ATTR ─────────────────────────────── */
|
||||
|
||||
#[test]
|
||||
fn attr_set_on_non_indexed_file_should_warn() {
|
||||
let tmp = tempdir().unwrap();
|
||||
marlin(&tmp).current_dir(tmp.path()).arg("init").assert().success();
|
||||
|
||||
let ghost = tmp.path().join("ghost.txt");
|
||||
std::fs::write(&ghost, "").unwrap();
|
||||
|
||||
marlin(&tmp)
|
||||
.args(["attr","set",
|
||||
&ghost.to_string_lossy(),"foo","bar"])
|
||||
.assert()
|
||||
.success() // exits 0
|
||||
.stderr(str::contains("not indexed"));
|
||||
}
|
||||
|
||||
/* ───────────────────── COLLECTIONS ───────────────────────────── */
|
||||
|
||||
#[test]
|
||||
fn coll_add_unknown_collection_should_fail() {
|
||||
let tmp = tempdir().unwrap();
|
||||
let file = tmp.path().join("doc.txt");
|
||||
std::fs::write(&file, "").unwrap();
|
||||
|
||||
marlin(&tmp).current_dir(tmp.path()).arg("init").assert().success();
|
||||
|
||||
marlin(&tmp)
|
||||
.args(["coll","add","nope",&file.to_string_lossy()])
|
||||
.assert()
|
||||
.failure();
|
||||
}
|
||||
|
||||
/* ───────────────────── RESTORE (bad file) ───────────────────── */
|
||||
|
||||
#[test]
|
||||
fn restore_with_nonexistent_backup_should_fail() {
|
||||
let tmp = tempdir().unwrap();
|
||||
|
||||
// create an empty DB first
|
||||
marlin(&tmp).arg("init").assert().success();
|
||||
|
||||
marlin(&tmp)
|
||||
.args(["restore", "/definitely/not/here.db"])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(str::contains("Failed to restore"));
|
||||
}
|
||||
|
172
cli-bin/tests/pos.rs
Normal file
172
cli-bin/tests/pos.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
//! tests pos.rs
|
||||
//! Positive-path integration checks for every sub-command
|
||||
//! that already has real logic behind it.
|
||||
|
||||
mod util;
|
||||
use util::marlin;
|
||||
|
||||
use predicates::{prelude::*, str}; // brings `PredicateBooleanExt::and`
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
/* ─────────────────────────── TAG ─────────────────────────────── */
|
||||
|
||||
#[test]
|
||||
fn tag_should_add_hierarchical_tag_and_search_finds_it() {
|
||||
let tmp = tempdir().unwrap();
|
||||
let file = tmp.path().join("foo.md");
|
||||
fs::write(&file, "# test\n").unwrap();
|
||||
|
||||
marlin(&tmp).current_dir(tmp.path()).arg("init").assert().success();
|
||||
|
||||
marlin(&tmp)
|
||||
.args(["tag", file.to_str().unwrap(), "project/md"])
|
||||
.assert().success();
|
||||
|
||||
marlin(&tmp)
|
||||
.args(["search", "tag:project/md"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(str::contains("foo.md"));
|
||||
}
|
||||
|
||||
/* ─────────────────────────── ATTR ────────────────────────────── */
|
||||
|
||||
#[test]
|
||||
fn attr_set_then_ls_roundtrip() {
|
||||
let tmp = tempdir().unwrap();
|
||||
let file = tmp.path().join("report.pdf");
|
||||
fs::write(&file, "%PDF-1.4\n").unwrap();
|
||||
|
||||
marlin(&tmp).current_dir(tmp.path()).arg("init").assert().success();
|
||||
|
||||
marlin(&tmp)
|
||||
.args(["attr", "set", file.to_str().unwrap(), "reviewed", "yes"])
|
||||
.assert().success();
|
||||
|
||||
marlin(&tmp)
|
||||
.args(["attr", "ls", file.to_str().unwrap()])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(str::contains("reviewed = yes"));
|
||||
}
|
||||
|
||||
/* ─────────────────────── COLLECTIONS ────────────────────────── */
|
||||
|
||||
#[test]
|
||||
fn coll_create_add_and_list() {
|
||||
let tmp = tempdir().unwrap();
|
||||
|
||||
let a = tmp.path().join("a.txt");
|
||||
let b = tmp.path().join("b.txt");
|
||||
fs::write(&a, "").unwrap();
|
||||
fs::write(&b, "").unwrap();
|
||||
|
||||
marlin(&tmp).current_dir(tmp.path()).arg("init").assert().success();
|
||||
|
||||
marlin(&tmp).args(["coll", "create", "Set"]).assert().success();
|
||||
for f in [&a, &b] {
|
||||
marlin(&tmp).args(["coll", "add", "Set", f.to_str().unwrap()]).assert().success();
|
||||
}
|
||||
|
||||
marlin(&tmp)
|
||||
.args(["coll", "list", "Set"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(str::contains("a.txt").and(str::contains("b.txt")));
|
||||
}
|
||||
|
||||
/* ─────────────────────────── VIEWS ───────────────────────────── */
|
||||
|
||||
#[test]
|
||||
fn view_save_list_and_exec() {
|
||||
let tmp = tempdir().unwrap();
|
||||
|
||||
let todo = tmp.path().join("TODO.txt");
|
||||
fs::write(&todo, "remember the milk\n").unwrap();
|
||||
|
||||
marlin(&tmp).current_dir(tmp.path()).arg("init").assert().success();
|
||||
|
||||
// save & list
|
||||
marlin(&tmp).args(["view", "save", "tasks", "milk"]).assert().success();
|
||||
marlin(&tmp)
|
||||
.args(["view", "list"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(str::contains("tasks: milk"));
|
||||
|
||||
// exec
|
||||
marlin(&tmp)
|
||||
.args(["view", "exec", "tasks"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(str::contains("TODO.txt"));
|
||||
}
|
||||
|
||||
/* ─────────────────────────── LINKS ───────────────────────────── */
|
||||
|
||||
#[test]
|
||||
fn link_add_rm_and_list() {
|
||||
let tmp = tempdir().unwrap();
|
||||
|
||||
let foo = tmp.path().join("foo.txt");
|
||||
let bar = tmp.path().join("bar.txt");
|
||||
fs::write(&foo, "").unwrap();
|
||||
fs::write(&bar, "").unwrap();
|
||||
|
||||
// handy closure
|
||||
let mc = || marlin(&tmp);
|
||||
|
||||
mc().current_dir(tmp.path()).arg("init").assert().success();
|
||||
mc().args(["scan", tmp.path().to_str().unwrap()]).assert().success();
|
||||
|
||||
// add
|
||||
mc().args(["link", "add", foo.to_str().unwrap(), bar.to_str().unwrap()])
|
||||
.assert().success();
|
||||
|
||||
// list (outgoing default)
|
||||
mc().args(["link", "list", foo.to_str().unwrap()])
|
||||
.assert().success()
|
||||
.stdout(str::contains("foo.txt").and(str::contains("bar.txt")));
|
||||
|
||||
// remove
|
||||
mc().args(["link", "rm", foo.to_str().unwrap(), bar.to_str().unwrap()])
|
||||
.assert().success();
|
||||
|
||||
// list now empty
|
||||
mc().args(["link", "list", foo.to_str().unwrap()])
|
||||
.assert().success()
|
||||
.stdout(str::is_empty());
|
||||
}
|
||||
|
||||
/* ─────────────────────── SCAN (multi-path) ───────────────────── */
|
||||
|
||||
#[test]
|
||||
fn scan_with_multiple_paths_indexes_all() {
|
||||
let tmp = tempdir().unwrap();
|
||||
|
||||
let dir_a = tmp.path().join("A");
|
||||
let dir_b = tmp.path().join("B");
|
||||
std::fs::create_dir_all(&dir_a).unwrap();
|
||||
std::fs::create_dir_all(&dir_b).unwrap();
|
||||
let f1 = dir_a.join("one.txt");
|
||||
let f2 = dir_b.join("two.txt");
|
||||
fs::write(&f1, "").unwrap();
|
||||
fs::write(&f2, "").unwrap();
|
||||
|
||||
marlin(&tmp).current_dir(tmp.path()).arg("init").assert().success();
|
||||
|
||||
// multi-path scan
|
||||
marlin(&tmp)
|
||||
.args(["scan", dir_a.to_str().unwrap(), dir_b.to_str().unwrap()])
|
||||
.assert().success();
|
||||
|
||||
// both files findable
|
||||
for term in ["one.txt", "two.txt"] {
|
||||
marlin(&tmp).args(["search", term])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(str::contains(term));
|
||||
}
|
||||
}
|
||||
|
68
cli-bin/tests/test.md
Normal file
68
cli-bin/tests/test.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Testing
|
||||
|
||||
Below is a **repeat-able 3-step flow** you can use **every time you pull fresh code**.
|
||||
|
||||
---
|
||||
|
||||
## 0 Prepare once
|
||||
|
||||
```bash
|
||||
# Run once (or add to ~/.bashrc) so debug + release artefacts land
|
||||
# in the same predictable place. Speeds-up future builds.
|
||||
export CARGO_TARGET_DIR=target
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1 Build the new binary
|
||||
|
||||
```bash
|
||||
git pull # grab the latest commit
|
||||
cargo build --release
|
||||
sudo install -Dm755 target/release/marlin /usr/local/bin/marlin
|
||||
```
|
||||
|
||||
* `cargo build --release` – builds the optimised binary.
|
||||
* `install …` – copies it into your `$PATH` so `marlin` on the CLI is the fresh one.
|
||||
|
||||
---
|
||||
|
||||
## 2 Run the smoke-test suite
|
||||
|
||||
```bash
|
||||
# Runs the end-to-end test we added in tests/e2e.rs
|
||||
cargo test --test e2e -- --nocapture
|
||||
```
|
||||
|
||||
* `--test e2e` – compiles and runs **only** `tests/e2e.rs`; other unit-tests are skipped (add them later if you like).
|
||||
* `--nocapture` – streams stdout/stderr so you can watch each CLI step in real time.
|
||||
* Exit-code **0** ➜ everything passed.
|
||||
Any non-zero exit or a red ✗ line means a step failed; the assert’s diff will show the command and its output.
|
||||
|
||||
---
|
||||
|
||||
## 3 (Optionally) run all tests
|
||||
|
||||
```bash
|
||||
cargo test --all -- --nocapture
|
||||
```
|
||||
|
||||
This will execute:
|
||||
|
||||
* unit tests in `src/**`
|
||||
* every file in `tests/`
|
||||
* doc-tests
|
||||
|
||||
If you wire **“cargo test --all”** into CI (GitHub Actions, GitLab, etc.), pushes that break a workflow will be rejected automatically.
|
||||
|
||||
---
|
||||
|
||||
### One-liner helper (copy/paste)
|
||||
|
||||
```bash
|
||||
cargo build --release &&
|
||||
sudo install -Dm755 target/release/marlin /usr/local/bin/marlin &&
|
||||
cargo test --all -- --nocapture
|
||||
```
|
||||
|
||||
Stick that in a shell alias (`alias marlin-ci='…'`) and you’ve got a 5-second upgrade-and-verify loop.
|
23
cli-bin/tests/util.rs
Normal file
23
cli-bin/tests/util.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
//! tests/util.rs
|
||||
//! Small helpers shared across integration tests.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use tempfile::TempDir;
|
||||
use assert_cmd::Command;
|
||||
/// Absolute path to the freshly-built `marlin` binary.
|
||||
pub fn bin() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_BIN_EXE_marlin"))
|
||||
}
|
||||
|
||||
/// Build a `Command` for `marlin` whose `MARLIN_DB_PATH` is
|
||||
/// `<tmp>/index.db`.
|
||||
///
|
||||
/// Each call yields a brand-new `Command`, so callers can freely add
|
||||
/// arguments, change the working directory, etc., without affecting
|
||||
/// other invocations.
|
||||
pub fn marlin(tmp: &TempDir) -> Command {
|
||||
let db_path: &Path = &tmp.path().join("index.db");
|
||||
let mut cmd = Command::new(bin());
|
||||
cmd.env("MARLIN_DB_PATH", db_path);
|
||||
cmd
|
||||
}
|
Reference in New Issue
Block a user