harn-cli 0.7.52

CLI for the Harn programming language — run, test, REPL, format, and lint
use super::*;
use serde::{Deserialize, Serialize};
use tokio::sync::MutexGuard;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct TriggerTables {
    #[serde(default)]
    pub(crate) triggers: Vec<TriggerManifestEntry>,
}

pub(crate) fn test_vm() -> harn_vm::Vm {
    let mut vm = harn_vm::Vm::new();
    harn_vm::register_vm_stdlib(&mut vm);
    vm
}

pub(crate) fn write_trigger_project(
    root: &Path,
    manifest: &str,
    lib_source: Option<&str>,
) -> PathBuf {
    std::fs::create_dir_all(root.join(".git")).unwrap();
    fs::write(root.join(MANIFEST), manifest).unwrap();
    if let Some(source) = lib_source {
        fs::write(root.join("lib.harn"), source).unwrap();
    }
    let harn_file = root.join("main.harn");
    fs::write(&harn_file, "pipeline main() {}\n").unwrap();
    harn_file
}

struct TestEnvGuard {
    previous_cwd: PathBuf,
    previous_cache: Option<std::ffi::OsString>,
    previous_registry: Option<std::ffi::OsString>,
    _cwd_lock: MutexGuard<'static, ()>,
    _env_lock: MutexGuard<'static, ()>,
}

impl Drop for TestEnvGuard {
    fn drop(&mut self) {
        std::env::set_current_dir(&self.previous_cwd).unwrap();
        if let Some(value) = self.previous_cache.clone() {
            std::env::set_var(HARN_CACHE_DIR_ENV, value);
        } else {
            std::env::remove_var(HARN_CACHE_DIR_ENV);
        }
        if let Some(value) = self.previous_registry.clone() {
            std::env::set_var(HARN_PACKAGE_REGISTRY_ENV, value);
        } else {
            std::env::remove_var(HARN_PACKAGE_REGISTRY_ENV);
        }
    }
}

pub(crate) fn with_test_env<T>(cwd: &Path, cache_dir: &Path, f: impl FnOnce() -> T) -> T {
    let cwd_lock = crate::tests::common::cwd_lock::lock_cwd();
    let env_lock = crate::tests::common::env_lock::lock_env().blocking_lock();
    let guard = TestEnvGuard {
        previous_cwd: std::env::current_dir().unwrap(),
        previous_cache: std::env::var_os(HARN_CACHE_DIR_ENV),
        previous_registry: std::env::var_os(HARN_PACKAGE_REGISTRY_ENV),
        _cwd_lock: cwd_lock,
        _env_lock: env_lock,
    };
    std::env::set_current_dir(cwd).unwrap();
    std::env::set_var(HARN_CACHE_DIR_ENV, cache_dir);
    std::env::remove_var(HARN_PACKAGE_REGISTRY_ENV);
    let result = f();
    drop(guard);
    result
}

pub(crate) fn run_git(repo: &Path, args: &[&str]) -> String {
    let output = test_git_command(repo).args(args).output().unwrap();
    if !output.status.success() {
        panic!(
            "git {:?} failed: {}",
            args,
            String::from_utf8_lossy(&output.stderr)
        );
    }
    String::from_utf8_lossy(&output.stdout).trim().to_string()
}

pub(crate) fn test_git_command(repo: &Path) -> process::Command {
    let mut command = process::Command::new("git");
    command
        .current_dir(repo)
        .env_remove("GIT_DIR")
        .env_remove("GIT_WORK_TREE")
        .env_remove("GIT_INDEX_FILE");
    command
}

pub(crate) fn create_git_package_repo_with(
    name: &str,
    manifest_tail: &str,
    lib_source: &str,
) -> (tempfile::TempDir, PathBuf, String) {
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path().join(name);
    fs::create_dir_all(&repo).unwrap();
    let init = test_git_command(&repo)
        .args(["init", "-b", "main"])
        .output()
        .unwrap();
    if !init.status.success() {
        let fallback = test_git_command(&repo).arg("init").output().unwrap();
        assert!(
            fallback.status.success(),
            "git init failed: {}",
            String::from_utf8_lossy(&fallback.stderr)
        );
    }
    run_git(&repo, &["config", "user.email", "tests@example.com"]);
    run_git(&repo, &["config", "user.name", "Harn Tests"]);
    run_git(&repo, &["config", "core.hooksPath", "/dev/null"]);
    fs::write(
        repo.join(MANIFEST),
        format!(
            r#"
[package]
name = "{name}"
version = "0.1.0"
"#
        ) + manifest_tail,
    )
    .unwrap();
    fs::write(repo.join("lib.harn"), lib_source).unwrap();
    run_git(&repo, &["add", "."]);
    run_git(&repo, &["commit", "-m", "initial"]);
    run_git(&repo, &["tag", "v1.0.0"]);
    let branch = run_git(&repo, &["branch", "--show-current"]);
    (tmp, repo, branch)
}

pub(crate) fn create_git_package_repo() -> (tempfile::TempDir, PathBuf, String) {
    create_git_package_repo_with(
        "acme-lib",
        "",
        "pub fn value() -> string { return \"v1\" }\n",
    )
}

pub(crate) fn write_package_registry_index(
    path: &Path,
    registry_name: &str,
    git: &str,
    package_name: &str,
) {
    fs::write(
        path,
        format!(
            r#"
version = 1

[[package]]
name = "{registry_name}"
description = "Acme package for registry tests"
repository = "{git}"
license = "MIT OR Apache-2.0"
harn = ">=0.7,<0.8"
exports = ["lib"]
connector_contract = "v1"
docs_url = "https://docs.example.test/acme"
checksum = "sha256:index"
provenance = "https://provenance.example.test/acme"

[[package.version]]
version = "1.0.0"
git = "{git}"
rev = "v1.0.0"
package = "{package_name}"
checksum = "sha256:package"
provenance = "https://provenance.example.test/acme/1.0.0"
"#
        ),
    )
    .unwrap();
}

pub(crate) fn test_harn_connector_source(provider_id: &str) -> String {
    format!(
        r#"
pub fn provider_id() {{
  return "{provider_id}"
}}

pub fn kinds() {{
  return ["webhook"]
}}

pub fn payload_schema() {{
  return {{
harn_schema_name: "EchoEventPayload",
json_schema: {{
  type: "object",
  additionalProperties: true,
}},
  }}
}}
"#
    )
}

pub(crate) fn write_publishable_package(root: &Path) {
    fs::create_dir_all(root.join("lib")).unwrap();
    fs::create_dir_all(root.join("docs")).unwrap();
    fs::write(
        root.join(MANIFEST),
        r#"[package]
name = "acme-lib"
version = "0.1.0"
description = "Acme helpers"
license = "MIT"
repository = "https://github.com/acme/acme-lib"
harn = ">=0.7,<0.8"
docs_url = "docs/api.md"

[exports]
lib = "lib/main.harn"

[dependencies]
"#,
    )
    .unwrap();
    fs::write(
        root.join("lib/main.harn"),
        r#"/// Return a greeting.
pub fn greet(name: string) -> string {
  return "hi " + name
}
"#,
    )
    .unwrap();
    fs::write(root.join("README.md"), "# acme-lib\n").unwrap();
    fs::write(root.join("LICENSE"), "MIT\n").unwrap();
    fs::write(root.join("docs/api.md"), "").unwrap();
}