oicana_cli 0.2.0

CLI for working with Oicana templates.
use anyhow::bail;
use clap::Args;
use log::{debug, trace};
use oicana::template::manifest::TemplateManifest;
use oicana_testing::{
    collect::{collect_tests, TemplateTests},
    SnapshotMode,
};
use std::{
    fs::read_to_string,
    path::{Path, PathBuf},
};
use walkdir::WalkDir;

#[derive(Debug, Args)]
pub struct TargetArgs {
    #[arg(help = "Target the template at the given path")]
    template: Option<String>,
    #[clap(
        long,
        short,
        help = "Target all templates in the directory and any subdirectories"
    )]
    all: bool,
}

impl TargetArgs {
    pub fn get_targets(&self) -> anyhow::Result<Vec<TemplateDir>> {
        let mut templates = vec![];
        let path = match self.template {
            None => Path::new("."),
            Some(ref template) => Path::new(template),
        };

        if self.all {
            for maybe_template in WalkDir::new(path) {
                let Ok(dir_entry) = maybe_template else {
                    continue;
                };
                if !dir_entry.path().is_dir() {
                    continue;
                }
                trace!("checking {:?}", dir_entry.path());
                if let Some(manifest) = is_path_oicana_template(dir_entry.path())? {
                    templates.push(TemplateDir {
                        path: dir_entry.into_path(),
                        manifest,
                    });
                }
            }
        } else if let Some(manifest) = is_path_oicana_template(path)? {
            templates.push(TemplateDir {
                path: path.to_path_buf(),
                manifest,
            });
        }
        if templates.is_empty() {
            bail!(
                "No valid Oicana template found {} {:?}.",
                if self.all { "in" } else { "at" },
                path.canonicalize()
                    .expect("Failed to canonicalize path for error message")
            );
        }
        debug!("Targeting templates: {templates:?}");

        Ok(templates)
    }
}

pub fn is_path_oicana_template(path: &Path) -> anyhow::Result<Option<TemplateManifest>> {
    if !path.exists() {
        bail!("No such file or directory.")
    }
    if !path.is_dir() {
        bail!("Given path is not a directory.")
    }
    let possible_manifest_path = path.join("typst.toml");
    match possible_manifest_path.try_exists() {
        Err(error) => {
            bail!("Error while checking for existence of {possible_manifest_path:?}: {error:?}");
        }
        Ok(false) => {
            debug!("Skipping {path:?} since it doesn't contain a 'typst.toml' file.");
            return Ok(None);
        }
        _ => (),
    }
    let manifest = match read_to_string(&possible_manifest_path) {
        Err(error) => {
            bail!("Failed to read manifest {possible_manifest_path:?}: {error:?}");
        }
        Ok(manifest) => manifest,
    };
    let manifest = match TemplateManifest::from_toml(&manifest) {
        Err(error) => {
            debug!("{possible_manifest_path:?} is not a valid Oicana template manifest: {error:?}");
            return Ok(None);
        }
        Ok(manifest) => manifest,
    };

    Ok(Some(manifest))
}

#[derive(Debug)]
pub struct TemplateDir {
    pub path: PathBuf,
    pub manifest: TemplateManifest,
}

impl TemplateDir {
    /// Collect all tests found in this template directory
    pub fn gather_tests(
        &self,
        snapshot_mode: SnapshotMode,
    ) -> Result<TemplateTests, anyhow::Error> {
        let test_dir = self.path.join(&self.manifest.tool.oicana.tests);
        if !test_dir.exists() || !test_dir.is_dir() {
            debug!(
                "Template {} does not have a test directory at {:?}.",
                self.manifest.package.name, test_dir
            );
            return Ok(TemplateTests::default());
        }

        let tests = collect_tests(&test_dir, snapshot_mode)?;

        Ok(tests)
    }
}

#[cfg(test)]
mod tests {
    use std::{fs::File, path::PathBuf};

    use oicana::template::{manifest::TemplateManifest, OicanaConfig};
    use oicana_testing::SnapshotMode;
    use tempfile::tempdir;
    use typst::syntax::package::PackageInfo;

    use super::TemplateDir;

    fn default_package_info() -> PackageInfo {
        PackageInfo::new("test-package", "0.1.0".parse().unwrap(), "main.typ")
    }

    #[test]
    fn no_tests_without_test_dir() {
        let tempdir = tempdir().unwrap();

        let template_dir = TemplateDir {
            path: tempdir.path().into(),
            manifest: TemplateManifest::new(
                default_package_info(),
                OicanaConfig {
                    manifest_version: 1,
                    inputs: vec![],
                    validate_json_inputs_by_default: true,
                    tests: PathBuf::from("tests"),
                    export: oicana::template::ExportConfig::default(),
                },
            ),
        };

        let mut result = template_dir.gather_tests(SnapshotMode::Compare);
        assert!(result.is_ok());
        assert!(result.unwrap().tests.is_empty());

        let temp_file = tempdir.path().join("tests");
        File::create(temp_file).unwrap();

        result = template_dir.gather_tests(SnapshotMode::Compare);
        assert!(result.is_ok());
        assert!(result.unwrap().tests.is_empty());
    }
}