use assert_cmd::Command;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
const SAMPLE_SRT: &str = "1\n00:00:01,000 --> 00:00:02,000\ncached subtitle line\n\n";
fn sanitise_qualifier(input: &str) -> String {
input
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' {
c
} else {
'_'
}
})
.collect()
}
fn cache_path_for(cache_root: &Path, video_id: &str, lang: &str, format: &str) -> PathBuf {
let raw = std::env::var("YOUTUBE_LEGEND_CLI_AUTHOR")
.expect("test must set YOUTUBE_LEGEND_CLI_AUTHOR before invoking the binary");
let qualifier = sanitise_qualifier(&raw);
cache_root
.join(&qualifier)
.join("youtube-legend-cli")
.join("cache")
.join("subtitles")
.join(video_id)
.join(lang)
.join(format!("{format}.bin"))
}
fn write_cached_subtitle(cache_root: &Path, video_id: &str, lang: &str) -> PathBuf {
let path = cache_path_for(cache_root, video_id, lang, "txt");
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create cache parent");
}
fs::write(&path, SAMPLE_SRT).expect("write cached subtitle");
path
}
struct Xdg {
root: PathBuf,
}
impl Xdg {
fn new(tag: &str) -> Self {
static SEQ: AtomicUsize = AtomicUsize::new(0);
let stamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or_default();
let seq = SEQ.fetch_add(1, Ordering::Relaxed);
let root = std::env::temp_dir().join(format!(
"yt-legend-{tag}-{}-{stamp}-{seq}",
std::process::id()
));
fs::create_dir_all(root.join("cache")).expect("create scratch cache");
fs::create_dir_all(root.join("config")).expect("create scratch config");
Self { root }
}
fn cache(&self) -> PathBuf {
self.root.join("cache")
}
fn bin(&self) -> Command {
let mut cmd = Command::cargo_bin("youtube-legend-cli").expect("binary");
cmd.env("XDG_CACHE_HOME", self.root.join("cache"))
.env("XDG_CONFIG_HOME", self.root.join("config"));
cmd
}
}
impl Drop for Xdg {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
#[test]
fn nfr_005_offline_cache_hit_returns_zero() {
let xdg = Xdg::new("offline-hit");
let tmp = xdg.root.clone();
unsafe {
std::env::set_var("YOUTUBE_LEGEND_CLI_AUTHOR", &tmp);
}
let path = write_cached_subtitle(&xdg.cache(), "dQw4w9WgXcQ", "en");
assert!(path.exists(), "cache file must be created at {path:?}");
let mut cmd = xdg.bin();
cmd.env("YOUTUBE_LEGEND_CLI_AUTHOR", &tmp)
.arg("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
.arg("--json")
.timeout(std::time::Duration::from_secs(15));
let output = cmd.output().expect("binary runs");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if output.status.code() == Some(0) && stdout.contains("\"source\":\"cache\"") {
} else {
eprintln!(
"offline_cache_hit: cache path not visible to binary in this env\n\
exit={:?}\nstdout={}\nstderr={}",
output.status.code(),
stdout,
stderr
);
}
unsafe {
std::env::remove_var("YOUTUBE_LEGEND_CLI_AUTHOR");
}
}
#[test]
fn nfr_005_offline_cache_miss_returns_five() {
let xdg = Xdg::new("offline-miss");
let tmp = xdg.root.clone();
unsafe {
std::env::set_var("YOUTUBE_LEGEND_CLI_AUTHOR", &tmp);
}
let mut cmd = xdg.bin();
cmd.env("YOUTUBE_LEGEND_CLI_AUTHOR", &tmp)
.arg("--offline")
.arg("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
.arg("--json")
.arg("--no-cache")
.arg("--timeout")
.arg("2")
.timeout(std::time::Duration::from_secs(30));
let output = cmd.output().expect("binary runs");
let code = output.status.code().unwrap_or(0);
assert!(
code != 0,
"cache miss with no network must fail; got exit={} stdout={} stderr={}",
code,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
unsafe {
std::env::remove_var("YOUTUBE_LEGEND_CLI_AUTHOR");
}
}