harn-cli 0.10.49

CLI for the Harn programming language — run, test, REPL, format, and lint
Documentation
use super::*;

use std::fs;

struct FixtureTestDir {
    inner: tempfile::TempDir,
}

impl FixtureTestDir {
    fn new() -> Self {
        Self {
            inner: tempfile::tempdir().unwrap(),
        }
    }

    fn write(&self, relative: &str, contents: &str) {
        let path = self.inner.path().join(relative);
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).unwrap();
        }
        fs::write(path, contents).unwrap();
    }
}

#[tokio::test]
async fn file_fixture_runs_once_and_cow_value_isolates_parameterized_cases() {
    let _env_guard = crate::tests::common::harn_state_lock::lock_harn_state_async().await;
    let temp = FixtureTestDir::new();
    temp.write(
        "suite/test_file_fixture.harn",
        r#"
@test_fixture(scope: file)
fn shared_fixture(harness: Harness) -> dict {
  harness.fs.append_locked("fixture-calls.log", "called\n")
  return {items: [1]}
}

@test(
  cases: [
    {name: "first", args: [10]},
    {name: "second", args: [20]},
  ],
  fixture: shared_fixture,
)
pipeline test_isolated(fx: dict, replacement: int) {
  assert_eq(fx.items[0], 1)
  let local = fx
  local.items[0] = replacement
  assert_eq(local.items[0], replacement)
}
"#,
    );

    let suite = temp.inner.path().join("suite");
    let summary = run_tests(&suite, None, 5_000, false, &[]).await;

    assert_eq!(summary.passed, 2, "{:?}", summary.results);
    assert_eq!(summary.failed, 0, "{:?}", summary.results);
    assert_eq!(
        fs::read_to_string(suite.join("fixture-calls.log")).unwrap(),
        "called\n",
        "file fixture must execute exactly once for all selected rows"
    );
}

#[tokio::test]
async fn case_fixture_runs_inside_each_fresh_case_vm() {
    let _env_guard = crate::tests::common::harn_state_lock::lock_harn_state_async().await;
    let temp = FixtureTestDir::new();
    temp.write(
        "suite/test_case_fixture.harn",
        r#"
let calls = 0

@test_fixture(scope: case)
fn fresh_fixture() -> dict {
  calls = calls + 1
  return {calls: calls}
}

@test(
  cases: [
    {name: "first", args: ["a"]},
    {name: "second", args: ["b"]},
  ],
  fixture: fresh_fixture,
)
pipeline test_fresh(fx: dict, label: string) {
  assert_eq(fx.calls, 1)
  assert(len(label) > 0)
}
"#,
    );

    let summary = run_tests(&temp.inner.path().join("suite"), None, 5_000, false, &[]).await;

    assert_eq!(summary.passed, 2, "{:?}", summary.results);
    assert_eq!(summary.failed, 0, "{:?}", summary.results);
}

#[tokio::test]
async fn file_fixture_failure_is_one_typed_result_and_suppresses_its_cases() {
    let _env_guard = crate::tests::common::harn_state_lock::lock_harn_state_async().await;
    let temp = FixtureTestDir::new();
    temp.write(
        "suite/test_fixture_failure.harn",
        r#"
@test_fixture(scope: file)
fn broken_fixture() -> int {
  assert(false, "fixture exploded")
  return 1
}

@test(
  cases: [
    {name: "first", args: []},
    {name: "second", args: []},
  ],
  fixture: broken_fixture,
)
pipeline test_suppressed(_fixture: int) {
  assert(false, "suppressed case executed")
}

pipeline test_unrelated() {}
"#,
    );

    let summary = run_tests(&temp.inner.path().join("suite"), None, 5_000, false, &[]).await;

    assert_eq!(summary.total, 2, "{:?}", summary.results);
    assert_eq!(summary.passed, 1, "{:?}", summary.results);
    assert_eq!(summary.failed, 1, "{:?}", summary.results);
    assert_eq!(
        summary
            .results
            .iter()
            .filter(|result| result.name == "<fixture broken_fixture>")
            .count(),
        1
    );
    assert!(summary
        .results
        .iter()
        .all(|result| !result.name.starts_with("test_suppressed[")));
}

#[tokio::test]
async fn fail_fast_stops_before_later_file_fixture_setup() {
    let _env_guard = crate::tests::common::harn_state_lock::lock_harn_state_async().await;
    let temp = FixtureTestDir::new();
    temp.write(
        "suite/a_broken.harn",
        r#"
@test_fixture(scope: file)
fn broken() -> int {
  assert(false, "stop here")
  return 1
}

@test(fixture: broken)
pipeline test_broken(_fixture: int) {}
"#,
    );
    temp.write(
        "suite/z_later.harn",
        r#"
@test_fixture(scope: file)
fn later(harness: Harness) -> int {
  harness.fs.append_locked("later-fixture-ran.log", "unexpected\n")
  return 1
}

@test(fixture: later)
pipeline test_later(_fixture: int) {}
"#,
    );

    let suite = temp.inner.path().join("suite");
    let options = RunOptions {
        timeout_ms: 5_000,
        parallel: true,
        fail_fast: true,
        jobs: Some(2),
        ..RunOptions::default()
    };
    let summary = run_tests_with_options(&suite, &options).await;

    assert_eq!(summary.total, 1, "{:?}", summary.results);
    assert_eq!(summary.results[0].name, "<fixture broken>");
    assert!(
        !suite.join("later-fixture-ran.log").exists(),
        "fail-fast must stop claiming fixture setup work after the first setup failure"
    );
}

#[tokio::test]
async fn fixture_contract_errors_are_source_located_discovery_failures() {
    let _env_guard = crate::tests::common::harn_state_lock::lock_harn_state_async().await;
    let temp = FixtureTestDir::new();
    temp.write(
        "suite/test_bad_fixture.harn",
        r"
@test_fixture(scope: suite)
fn invalid_scope() -> int { return 1 }

@test(fixture: missing_fixture)
pipeline test_missing(_fixture: int) {}
",
    );

    let summary = run_tests(&temp.inner.path().join("suite"), None, 5_000, false, &[]).await;

    assert_eq!(summary.total, 1);
    let error = summary.results[0].error.as_deref().unwrap();
    assert!(
        error.starts_with("2:"),
        "diagnostic must begin with source line and column: {error}"
    );
    assert!(error.contains("must be `file` or `case`"), "{error}");
}

#[tokio::test]
async fn malformed_parameterized_rows_are_source_located_discovery_failures() {
    let _env_guard = crate::tests::common::harn_state_lock::lock_harn_state_async().await;
    let temp = FixtureTestDir::new();
    temp.write(
        "suite/test_parameterized.harn",
        r#"
@test(cases: [
  {name: "duplicate", args: [1]},
  {name: "duplicate", args: [2]},
])
pipeline test_value(value) { assert(false, "must not execute") }
"#,
    );

    let summary = run_tests(&temp.inner.path().join("suite"), None, 5_000, false, &[]).await;

    assert_eq!(summary.total, 1);
    assert_eq!(summary.results[0].name, "<file error>");
    assert_eq!(summary.timing.sample_count, 0);
    let error = summary.results[0].error.as_deref().unwrap();
    assert!(error.starts_with("4:"), "{error}");
    assert!(error.contains("non-empty and unique"), "{error}");
}