use std::path::Path;
use std::process::{Command, Stdio};
fn playr(db: &Path, args: &[&str]) {
Command::new(env!("CARGO_BIN_EXE_playr"))
.env("XDG_CONFIG_HOME", db.parent().unwrap())
.env("XDG_DATA_HOME", db.parent().unwrap())
.arg("--db")
.arg(db)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.unwrap();
}
#[test]
fn only_scan_creates_the_library() {
let dir = tempfile::tempdir().unwrap();
let db = dir.path().join("playr/library.db");
let file = dir.path().join("x.mp3");
std::fs::write(&file, b"x").unwrap();
for args in [
vec![file.to_str().unwrap()],
vec![],
vec!["search", "anything"],
vec!["playlist", "late"],
vec!["playlists"],
] {
playr(&db, &args);
assert!(
!db.exists(),
"`playr {}` created the library",
args.join(" ")
);
}
playr(&db, &["scan", dir.path().to_str().unwrap()]);
assert!(db.exists(), "`playr scan` did not create the library");
}
fn output(db: &Path, args: &[&str]) -> (Option<i32>, String, String) {
let out = Command::new(env!("CARGO_BIN_EXE_playr"))
.env("XDG_CONFIG_HOME", db.parent().unwrap())
.env("XDG_DATA_HOME", db.parent().unwrap())
.arg("--db")
.arg(db)
.args(args)
.stdin(Stdio::null())
.output()
.unwrap();
(
out.status.code(),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
#[test]
fn bad_arguments_are_refused_before_anything_runs() {
let dir = tempfile::tempdir().unwrap();
let db = dir.path().join("library.db");
for (args, error) in [
(&["--bogus"][..], "unexpected argument '--bogus'"),
(&["playlists", "--bogus"], "unexpected argument '--bogus'"),
(&["formats", "extra"], "unexpected argument 'extra'"),
(&["search", "rock", "--db"], "a value is required for '--db"),
(&["search"], "<QUERY>"),
(&["scan", "--bogus"], "unexpected argument '--bogus'"),
] {
let (code, _, stderr) = output(&db, args);
assert_eq!(code, Some(2), "playr {args:?}: {stderr}");
assert!(stderr.contains(error), "playr {args:?}: {stderr}");
}
let (code, _, stderr) = output(&db, &["scan"]);
assert_eq!(code, Some(1), "bare scan with no library: {stderr}");
assert!(
stderr.contains("no directories to scan"),
"bare scan with no library: {stderr}"
);
let (code, _, stderr) = output(&db, &["prune"]);
assert_eq!(code, Some(1), "bare prune with no library: {stderr}");
assert!(
stderr.contains("no library"),
"bare prune with no library: {stderr}"
);
assert!(!db.exists());
}
#[test]
fn each_subcommand_has_its_own_help() {
let dir = tempfile::tempdir().unwrap();
let db = dir.path().join("library.db");
for (command, text) in [
("scan", "scan [OPTIONS] [DIR]..."),
("prune", "prune [OPTIONS] [DIR]..."),
("search", "--json"),
] {
let (code, stdout, _) = output(&db, &[command, "--help"]);
assert_eq!(code, Some(0));
assert!(stdout.contains(text), "playr {command} --help: {stdout}");
}
assert!(!db.exists(), "playr scan --help scanned");
}
#[test]
fn search_json_prints_every_match_with_its_columns() {
use playr_core::db::{self, Track};
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("library.db");
let conn = db::open(&db_path).unwrap();
for track in [
Track {
path: "/m/peace piece.flac".into(),
title: Some("Peace Piece".into()),
artist: Some("Bill Evans".into()),
track_no: Some(3),
duration_ms: Some(393_000),
mtime: 1,
size: 2,
..Default::default()
},
Track {
path: "/m/evans-untagged.wav".into(),
mtime: 1,
size: 2,
..Default::default()
},
Track {
path: "/m/other.flac".into(),
title: Some("Other".into()),
mtime: 1,
size: 2,
..Default::default()
},
] {
db::upsert(&conn, &track).unwrap();
}
drop(conn);
let (code, stdout, stderr) = output(&db_path, &["search", "--json", "evans"]);
assert_eq!(code, Some(0), "{stderr}");
let rows: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let rows = rows.as_array().unwrap();
let paths: Vec<&str> = rows.iter().map(|r| r["path"].as_str().unwrap()).collect();
assert_eq!(paths.len(), 2, "{stdout}");
assert!(paths.contains(&"/m/evans-untagged.wav"), "{stdout}");
let piece = rows
.iter()
.find(|r| r["path"] == "/m/peace piece.flac")
.unwrap();
assert_eq!(piece["title"], "Peace Piece");
assert_eq!(piece["artist"], "Bill Evans");
assert_eq!(piece["track_no"], 3);
assert_eq!(piece["duration_ms"], 393_000);
assert!(piece["album"].is_null(), "{piece}");
assert!(piece["id"].as_i64().unwrap() > 0);
assert_eq!(piece.as_object().unwrap().len(), 16, "{piece}");
let (code, stdout, _) = output(&db_path, &["search", "--json", "coltrane"]);
assert_eq!((code, stdout.trim()), (Some(1), "[]"));
let (code, stdout, stderr) = output(&db_path, &["search", "--json", "--", "--db"]);
assert_eq!((code, stdout.trim()), (Some(1), "[]"), "{stderr}");
}
fn with_config(dir: &Path, args: &[&str]) -> (bool, String) {
let out = Command::new(env!("CARGO_BIN_EXE_playr"))
.env("XDG_CONFIG_HOME", dir)
.env("XDG_DATA_HOME", dir)
.arg("--db")
.arg(dir.join("library.db"))
.args(args)
.stdin(Stdio::null())
.output()
.unwrap();
(
out.status.success(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
#[test]
fn bad_settings_stop_playr_before_it_starts_and_list_every_error() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join("playr")).unwrap();
let default = dir.path().join("playr/settings.toml");
std::fs::write(&default, "volume = 50\nfrob = 1\n[keys]\nd = 'remove'\n").unwrap();
let (ok, stderr) = with_config(dir.path(), &[]);
assert!(!ok);
let path = default.display();
assert!(
stderr.contains(&format!("playr: {path}: line 2: unknown setting: frob")),
"{stderr}"
);
assert!(
stderr.contains(&format!("playr: {path}: line 4: :remove works")),
"{stderr}"
);
assert!(!stderr.contains("terminal"), "{stderr}");
let other = dir.path().join("other.toml");
std::fs::write(&other, "volume = 50\n").unwrap();
let (_, stderr) = with_config(dir.path(), &["--settings", other.to_str().unwrap()]);
assert!(
!stderr.contains("line 2"),
"the default file was read: {stderr}"
);
let (ok, stderr) = with_config(dir.path(), &["--settings", "/nonexistent/playr.toml"]);
assert!(!ok);
assert!(
stderr.contains("playr: /nonexistent/playr.toml:"),
"{stderr}"
);
}
#[test]
fn a_missing_home_variable_does_not_stop_playr() {
let dir = tempfile::tempdir().unwrap();
let settings = dir.path().join("settings.toml");
std::fs::write(&settings, "frob = 1\n").unwrap();
let out = Command::new(env!("CARGO_BIN_EXE_playr"))
.env_remove("HOME")
.env_remove("XDG_CONFIG_HOME")
.env("XDG_DATA_HOME", dir.path())
.arg("--db")
.arg(dir.path().join("library.db"))
.arg("--settings")
.arg(&settings)
.stdin(Stdio::null())
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(!stderr.contains("panicked"), "{stderr}");
assert!(stderr.contains("line 1: unknown setting: frob"), "{stderr}");
}
#[test]
fn a_second_playr_is_refused_but_reading_the_library_is_not() {
use playr_app::instance::{claim_at, ALREADY_RUNNING};
let dir = tempfile::tempdir().unwrap();
let data = dir.path().join("data");
let db = dir.path().join("library.db");
let songs = dir.path().join("music");
std::fs::create_dir(&songs).unwrap();
let _running = claim_at(&data.join("playr/instance.lock")).unwrap();
let run = |args: &[&str]| {
Command::new(env!("CARGO_BIN_EXE_playr"))
.env("XDG_DATA_HOME", &data)
.env("XDG_CONFIG_HOME", dir.path())
.arg("--db")
.arg(&db)
.args(args)
.stdin(Stdio::null())
.output()
.unwrap()
};
for args in [vec!["scan", songs.to_str().unwrap()], vec![]] {
let out = run(&args);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(!out.status.success(), "playr {args:?} ran");
assert!(stderr.contains(ALREADY_RUNNING), "playr {args:?}: {stderr}");
}
assert!(!db.exists(), "a refused scan created the library");
for args in [&["playlists"][..], &["formats"], &["search", "--json", "x"]] {
let out = run(args);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains(ALREADY_RUNNING),
"playr {args:?}: {stderr}"
);
}
}
#[test]
fn scan_keeps_missing_tracks_and_prune_removes_them_with_their_marks() {
use playr_core::db::{self, query, Track};
let dir = tempfile::tempdir().unwrap();
let music = dir.path().canonicalize().unwrap().join("music");
std::fs::create_dir(&music).unwrap();
let db_path = dir.path().join("library.db");
let gone = music.join("gone.flac").to_string_lossy().into_owned();
let conn = db::open(&db_path).unwrap();
let track = Track {
path: gone.clone(),
mtime: 1,
size: 1,
..Default::default()
};
db::upsert(&conn, &track).unwrap();
query::add_mark(
&conn,
&gone,
query::Mark {
frame: 1,
rate: 8000,
},
)
.unwrap();
drop(conn);
let (code, stdout, stderr) = output(&db_path, &["scan", music.to_str().unwrap()]);
assert_eq!(code, Some(0), "{stderr}");
assert!(stdout.contains("1 tracks missing"), "{stdout}");
let conn = db::open(&db_path).unwrap();
assert_eq!(query::count(&conn).unwrap(), 1, "a scan removed a track");
let (code, stdout, stderr) = output(&db_path, &["prune", music.to_str().unwrap()]);
assert_eq!(code, Some(0), "{stderr}");
assert!(stdout.contains("removed 1 tracks and 1 marks"), "{stdout}");
assert_eq!(query::count(&conn).unwrap(), 0);
assert!(query::marks(&conn, &gone).unwrap().is_empty());
let other = dir.path().join("other.db");
let (code, _, stderr) = output(&other, &["prune", music.to_str().unwrap()]);
assert_eq!(code, Some(1), "{stderr}");
assert!(stderr.contains("no library"), "{stderr}");
assert!(!other.exists());
}
#[test]
fn roots_lists_what_was_scanned_and_rm_forgets_it() {
let dir = tempfile::tempdir().unwrap();
let db = dir.path().join("playr/library.db");
let songs = dir.path().join("music");
std::fs::create_dir(&songs).unwrap();
std::fs::write(songs.join("a.wav"), b"not audio").unwrap();
let (code, _, stderr) = output(&db, &["roots"]);
assert_eq!(code, Some(1), "{stderr}");
assert!(stderr.contains("no library"), "{stderr}");
playr(&db, &["scan", songs.to_str().unwrap()]);
let canonical = songs.canonicalize().unwrap();
let (code, stdout, _) = output(&db, &["roots"]);
assert_eq!(code, Some(0));
assert_eq!(stdout.trim(), canonical.display().to_string());
let (code, _, stderr) = output(&db, &["roots", "rm", dir.path().to_str().unwrap()]);
assert_eq!(code, Some(1), "{stderr}");
assert!(stderr.contains("not one of the library's"), "{stderr}");
let (code, stdout, stderr) = output(&db, &["roots", "rm", songs.to_str().unwrap()]);
assert_eq!(code, Some(0), "{stderr}");
assert!(stdout.contains("forgot"), "{stdout}");
let (_, stdout, _) = output(&db, &["roots"]);
assert!(stdout.trim().is_empty(), "still listed: {stdout}");
}