u8loc 1.1.2

Run child processes in a UTF-8-capable locale
#![deny(missing_docs)]
// SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
// SPDX-License-Identifier: BSD-2-Clause
//! Run the Python functional test against the built `u8loc` tool.

#![expect(clippy::print_stdout, reason = "test suite")]
#![expect(clippy::tests_outside_test_module, reason = "integration test")]
#![expect(clippy::use_debug, reason = "test suite")]

use std::env::{self, VarError};
use std::process::{Command, Stdio};
use std::sync::LazyLock;

use anyhow::{Context as _, Result, bail};
use camino::{Utf8Path, Utf8PathBuf};
use roundlet::test_exe::{self, PathsMapResult};

fn get_exe_path() -> Result<&'static Utf8Path> {
    static EXE_NAMES: [&str; 1] = ["u8loc"];
    static PATHS_RES: LazyLock<PathsMapResult<'_>> =
        LazyLock::new(|| test_exe::find_exe_paths(&EXE_NAMES));

    (*PATHS_RES)
        .get_exe_path("u8loc")
        .context("find/get_exe_path(u8loc)")
}

fn find_uv() -> Result<Option<String>> {
    let uv_prog = match env::var("UV") {
        Ok(prog) => prog,
        Err(VarError::NotPresent) => "uv".to_owned(),
        Err(VarError::NotUnicode(_)) => {
            bail!("Could not parse the UV environment variable as a UTF-8 string");
        }
    };

    let res = match Command::new(&uv_prog)
        .arg("--version")
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
    {
        Ok(res) => res,
        Err(err) => {
            println!("Could not run `{uv_prog} --version`: {err}");
            return Ok(None);
        }
    };
    if !res.status.success() {
        println!("The `{uv_prog} --version` command failed: {res:?}");
        return Ok(None);
    }

    let output = match String::from_utf8(res.stdout) {
        Ok(output) => output,
        Err(err) => {
            println!("Could not parse the output of `{uv_prog} --version` as valid UTF-8: {err}");
            return Ok(None);
        }
    };
    if !output.starts_with("uv ") {
        println!("The output of `{uv_prog} --version` did not start with 'uv': {output:?}");
        return Ok(None);
    }

    println!("Looks like we can use {uv_prog}");
    Ok(Some(uv_prog))
}

/// Try to determine the top-level source directory.
fn find_topdir() -> Result<Utf8PathBuf> {
    let u8loc: Utf8PathBuf = env!("CARGO_MANIFEST_DIR").into();
    let Some(rust) = u8loc.parent() else {
        bail!("Could not get the parent directory of {u8loc}");
    };
    let Some(topdir) = rust.parent() else {
        bail!("Could not get the parent directory of {rust}");
    };
    Ok(topdir.into())
}

/// Run the Python functional test against the built `u8loc` tool.
#[test]
fn functional_python() -> Result<()> {
    let exe = get_exe_path()?;
    let topdir = find_topdir()?;
    let functest = topdir.join("tests").join("functional.py");
    if !functest.is_file() {
        println!("No {functest} file, skipping the Python functional test");
        return Ok(());
    }
    let Some(uv_prog) = find_uv()? else {
        println!("No suitable `uv` program, skipping the Python functional test");
        return Ok(());
    };

    println!("Setting up the Python virtual environment for the unit tests");
    if !Command::new(&uv_prog)
        .args(["sync", "--group", "testenv-unit-tests"])
        .current_dir(&topdir)
        .stdin(Stdio::null())
        .status()
        .with_context(|| format!("Could not run `{uv_prog} --sync`"))?
        .success()
    {
        bail!("`{uv_prog} sync` failed");
    }

    println!("Running the functional test against {exe}");
    let python = topdir.join(".venv").join("bin").join("python3");
    if !Command::new(&python)
        .args(["--", functest.as_ref(), "-p", exe.as_ref()])
        .stdin(Stdio::null())
        .status()
        .with_context(|| format!("Could not run {python}"))?
        .success()
    {
        bail!("The functional tests failed for {exe}");
    }
    Ok(())
}