mediapulse 0.1.5

A self-contained native media player built around mpv
#![cfg(unix)]

use std::fs::{self, File, set_permissions};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::Command;

use tempfile::tempdir;

fn fake_sidecar(directory: &Path, exit_code: i32) -> PathBuf {
    let path = directory.join("mpv");
    let mut file = File::create(&path).expect("fake sidecar file");
    std::io::Write::write_all(
        &mut file,
        format!("#!/bin/sh\nprintf '<%s>\\n' \"$@\"\nexit {exit_code}\n").as_bytes(),
    )
    .expect("fake sidecar script");
    set_permissions(&path, std::fs::Permissions::from_mode(0o755)).expect("sidecar permissions");
    path
}

#[test]
fn mp_forwards_canonical_and_legacy_arguments_unchanged() {
    let temp = tempdir().expect("temp directory");
    let sidecar = fake_sidecar(temp.path(), 7);
    let canonical = ["--volume=80", "--no-fullscreen", "--speed=1.5", "video.mkv"];
    let legacy = [
        "-volume",
        "80",
        "-no-fullscreen",
        "-speed",
        "1.5",
        "video.mkv",
    ];
    let cases: &[&[&str]] = &[&canonical, &legacy];

    for &arguments in cases {
        let output = Command::new(env!("CARGO_BIN_EXE_mp"))
            .env("MEDIAPULSE_MPV_PATH", &sidecar)
            .args(arguments)
            .output()
            .expect("run mp");

        assert_eq!(output.status.code(), Some(7));
        let expected = arguments
            .iter()
            .fold(String::new(), |mut output, argument| {
                use std::fmt::Write as _;
                writeln!(output, "<{argument}>").expect("write to String");
                output
            });
        assert_eq!(
            String::from_utf8(output.stdout).expect("UTF-8 stdout"),
            expected
        );
        assert!(output.stderr.is_empty());
    }
}

#[test]
fn mp_forwards_list_options_to_the_bundled_engine() {
    let temp = tempdir().expect("temp directory");
    let sidecar = fake_sidecar(temp.path(), 0);

    let output = Command::new(env!("CARGO_BIN_EXE_mp"))
        .env("MEDIAPULSE_MPV_PATH", &sidecar)
        .args(["--no-config", "--list-options"])
        .output()
        .expect("run mp");

    assert!(output.status.success());
    assert_eq!(
        String::from_utf8(output.stdout).expect("UTF-8 stdout"),
        "<--no-config>\n<--list-options>\n"
    );
}

#[test]
fn mp_help_and_version_work_without_an_engine() {
    let temp = tempdir().expect("temp directory");
    let isolated_executable = temp.path().join("mp");
    fs::copy(env!("CARGO_BIN_EXE_mp"), &isolated_executable).expect("copy mp executable");
    set_permissions(&isolated_executable, std::fs::Permissions::from_mode(0o755))
        .expect("mp permissions");

    for flag in ["--help", "-h", "--version", "-V"] {
        let output = Command::new(&isolated_executable)
            .env_remove("MEDIAPULSE_MPV_PATH")
            .env_remove("MEDIAPULSE_RESOURCE_DIR")
            .env("PATH", "")
            .arg(flag)
            .output()
            .expect("run mp");

        assert_eq!(
            output.status.code(),
            Some(0),
            "{flag} must succeed without an mpv engine"
        );
        let stdout = String::from_utf8(output.stdout).expect("UTF-8 stdout");
        assert!(stdout.contains(env!("CARGO_PKG_VERSION")));
    }
}

#[test]
fn mp_without_arguments_prints_usage_instead_of_a_bare_engine_error() {
    let temp = tempdir().expect("temp directory");
    let isolated_executable = temp.path().join("mp");
    fs::copy(env!("CARGO_BIN_EXE_mp"), &isolated_executable).expect("copy mp executable");
    set_permissions(&isolated_executable, std::fs::Permissions::from_mode(0o755))
        .expect("mp permissions");

    let output = Command::new(&isolated_executable)
        .env_remove("MEDIAPULSE_MPV_PATH")
        .env_remove("MEDIAPULSE_RESOURCE_DIR")
        .env("PATH", "")
        .output()
        .expect("run mp");

    assert_eq!(output.status.code(), Some(1));
    let stderr = String::from_utf8(output.stderr).expect("UTF-8 stderr");
    assert!(stderr.contains("Usage:"));
    assert!(stderr.contains("MEDIAPULSE_MPV_PATH=/absolute/path/to/mpv"));
}

#[test]
fn mp_never_falls_back_to_system_path() {
    let temp = tempdir().expect("temp directory");
    let isolated_executable = temp.path().join("mp");
    fs::copy(env!("CARGO_BIN_EXE_mp"), &isolated_executable).expect("copy mp executable");
    set_permissions(&isolated_executable, std::fs::Permissions::from_mode(0o755))
        .expect("mp permissions");

    let output = Command::new(&isolated_executable)
        .env_remove("MEDIAPULSE_MPV_PATH")
        .env_remove("MEDIAPULSE_RESOURCE_DIR")
        .env("PATH", "")
        .arg("video.mkv")
        .output()
        .expect("run mp");

    assert_eq!(output.status.code(), Some(1));
    let stderr = String::from_utf8(output.stderr).expect("UTF-8 stderr");
    assert!(stderr.contains("Cargo installs do not include native mpv"));
    assert!(stderr.contains("MEDIAPULSE_MPV_PATH=/absolute/path/to/mpv"));
    assert!(stderr.contains("MediaPulse desktop bundle"));
    assert!(stderr.contains("bundled mpv sidecar was not found"));
}