chordlib 0.15.0

Work with chord-and-lyrics songs: parse, transform, and render them to multiple formats
Documentation
#![cfg(feature = "bin")]

use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::time::{SystemTime, UNIX_EPOCH};

struct TestDir(PathBuf);

impl TestDir {
    fn new(name: &str) -> Self {
        let unique = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("system clock")
            .as_nanos();
        let path = std::env::temp_dir().join(format!(
            "chordlib-propresenter-{name}-{}-{unique}",
            std::process::id()
        ));
        fs::create_dir(&path).expect("create test directory");
        Self(path)
    }

    fn join(&self, path: &str) -> PathBuf {
        self.0.join(path)
    }
}

impl Drop for TestDir {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.0);
    }
}

fn run(args: &[&Path]) -> Output {
    let mut command = Command::new(env!("CARGO_BIN_EXE_chordlib"));
    for argument in args {
        command.arg(argument);
    }
    command.output().expect("run chordlib CLI")
}

#[test]
fn converts_mixed_case_chordpro_to_pro_and_selects_language_and_key() {
    let directory = TestDir::new("round-trip");
    let input = directory.join("input.WP");
    let presentation = directory.join("output.PrO");
    let result = directory.join("result.cP");
    fs::write(
        &input,
        "{title: Languages}\n{language: de en}\n{key: C}\n{section: Verse}\n[C]Hallo\n&[C]Hello\n",
    )
    .expect("write input");

    let output = Command::new(env!("CARGO_BIN_EXE_chordlib"))
        .arg(&input)
        .arg("--output")
        .arg(&presentation)
        .arg("--language")
        .arg("1")
        .arg("--key")
        .arg("5")
        .arg("--nashville")
        .output()
        .expect("export ProPresenter");
    assert!(
        output.status.success(),
        "export failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(fs::metadata(&presentation).expect("presentation").len() > 0);

    let output = run(&[&presentation, Path::new("--output"), &result]);
    assert!(
        output.status.success(),
        "import failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    let converted = fs::read_to_string(result).expect("read converted ChordPro");
    assert!(converted.contains("Hello"));
    assert!(!converted.contains("Hallo"));
    assert!(converted.contains("[D]"), "converted output: {converted}");
}

#[test]
fn routes_chopro_as_text_and_reports_malformed_protobuf() {
    let directory = TestDir::new("routing");
    let chordpro = directory.join("song.ChOpRo");
    let presentation = directory.join("song.PRO");
    fs::write(
        &chordpro,
        "{title: Routed}\n{key: C}\n{section: Verse}\n[C]Text\n",
    )
    .expect("write ChordPro");
    let output = run(&[&chordpro, Path::new("--output"), &presentation]);
    assert!(
        output.status.success(),
        "ChordPro route failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let malformed = directory.join("malformed.pro");
    let target = directory.join("unused.cp");
    fs::write(&malformed, b"not a protobuf").expect("write malformed input");
    let output = run(&[&malformed, Path::new("--output"), &target]);
    assert!(!output.status.success());
    assert!(String::from_utf8_lossy(&output.stderr).contains("invalid ProPresenter protobuf"));
}