Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ result
.claude-reliability/
.claude/reliability-config.yml

lcov.info

# claude-reliability managed
.claude/bin/
.claude/*.local.md
Expand Down
3 changes: 3 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
RELEASE_TYPE: patch

Add unit tests for protocol, generators, and runner internals.
85 changes: 85 additions & 0 deletions src/antithesis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,88 @@ pub(crate) fn emit_assertion(location: &TestLocation, passed: bool) {
writeln!(file, "{}", serde_json::to_string(&declaration).unwrap()).unwrap();
writeln!(file, "{}", serde_json::to_string(&evaluation).unwrap()).unwrap();
}

#[cfg(test)]
mod tests {
use super::*;
use crate::ENV_TEST_MUTEX;

#[test]
fn test_is_running_in_antithesis_with_valid_dir() {
let _guard = ENV_TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().to_str().unwrap().to_string();
let original = std::env::var("ANTITHESIS_OUTPUT_DIR").ok();
// SAFETY: serialized by ENV_LOCK
unsafe { std::env::set_var("ANTITHESIS_OUTPUT_DIR", &path) };
assert!(is_running_in_antithesis());
match original {
Some(v) => unsafe { std::env::set_var("ANTITHESIS_OUTPUT_DIR", v) },
None => unsafe { std::env::remove_var("ANTITHESIS_OUTPUT_DIR") },
}
}

#[test]
fn test_is_running_in_antithesis_without_env() {
let _guard = ENV_TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let original = std::env::var("ANTITHESIS_OUTPUT_DIR").ok();
if original.is_some() {
unsafe { std::env::remove_var("ANTITHESIS_OUTPUT_DIR") };
}
assert!(!is_running_in_antithesis());
if let Some(v) = original {
unsafe { std::env::set_var("ANTITHESIS_OUTPUT_DIR", v) };
}
}

#[cfg(feature = "antithesis")]
#[test]
fn test_emit_assertion_writes_jsonl() {
let _guard = ENV_TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().to_str().unwrap().to_string();
let original = std::env::var("ANTITHESIS_OUTPUT_DIR").ok();
// SAFETY: serialized by ENV_LOCK
unsafe { std::env::set_var("ANTITHESIS_OUTPUT_DIR", &path) };

let loc = TestLocation {
function: "test_func_42".into(),
file: "test_file.rs".into(),
class: "test_module".into(),
begin_line: 99,
};
emit_assertion(&loc, true);

match original {
Some(v) => unsafe { std::env::set_var("ANTITHESIS_OUTPUT_DIR", v) },
None => unsafe { std::env::remove_var("ANTITHESIS_OUTPUT_DIR") },
}

let jsonl_path = dir.path().join("sdk.jsonl");
assert!(jsonl_path.exists());
let contents = std::fs::read_to_string(&jsonl_path).unwrap();
let lines: Vec<&str> = contents.lines().collect();
assert_eq!(lines.len(), 2);
assert!(contents.contains("test_func_42"));
assert!(contents.contains("test_module"));
}

#[test]
#[should_panic(expected = "Expected ANTITHESIS_OUTPUT_DIR")]
fn test_is_running_in_antithesis_with_nonexistent_dir() {
let _guard = ENV_TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let original = std::env::var("ANTITHESIS_OUTPUT_DIR").ok();
unsafe {
std::env::set_var(
"ANTITHESIS_OUTPUT_DIR",
"/nonexistent/path/for/coverage/test",
)
};
let _result = is_running_in_antithesis();
// Restore (won't reach here due to panic, but keep for completeness)
match original {
Some(v) => unsafe { std::env::set_var("ANTITHESIS_OUTPUT_DIR", v) },
None => unsafe { std::env::remove_var("ANTITHESIS_OUTPUT_DIR") },
}
}
}
39 changes: 39 additions & 0 deletions src/cbor_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,43 @@ mod tests {
let v = cbor_serialize(&"hello");
assert_eq!(as_text(&v), Some("hello"));
}

#[test]
fn test_as_text_returns_none_for_non_text() {
assert_eq!(as_text(&Value::from(42)), None);
assert_eq!(as_text(&Value::Bool(true)), None);
}

#[test]
fn test_as_u64_returns_none_for_non_integer() {
assert_eq!(as_u64(&Value::Text("hello".into())), None);
assert_eq!(as_u64(&Value::Bool(false)), None);
}

#[test]
#[should_panic(expected = "expected Value::Map")]
fn test_map_get_panics_on_non_map() {
map_get(&Value::from(42), "key");
}

#[test]
#[should_panic(expected = "expected Value::Text")]
fn test_map_get_panics_on_non_text_key() {
let bad_map = Value::Map(vec![(Value::from(42), Value::from("val"))]);
map_get(&bad_map, "key");
}

#[test]
#[should_panic(expected = "expected Value::Map")]
fn test_map_insert_panics_on_non_map() {
let mut val = Value::from(42);
map_insert(&mut val, "key", "value");
}

#[test]
#[should_panic(expected = "expected Value::Text")]
fn test_map_insert_panics_on_non_text_key() {
let mut bad_map = Value::Map(vec![(Value::from(42), Value::from("val"))]);
map_insert(&mut bad_map, "key", "value");
}
}
11 changes: 11 additions & 0 deletions src/control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ pub(crate) fn with_test_context<R>(f: impl FnOnce() -> R) -> R {
result
}

/// Extract a message from a panic payload.
pub(crate) fn panic_message(payload: &Box<dyn std::any::Any + Send>) -> String {
if let Some(s) = payload.downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"Unknown panic".to_string()
}
}

/// Returns `true` if we are currently inside a Hegel test context.
///
/// This can be used to conditionally execute code that depends on a
Expand Down
24 changes: 24 additions & 0 deletions src/generators/compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,27 @@ macro_rules! compose {
})
}};
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_fnv1a_hash_deterministic() {
let hash1 = fnv1a_hash(b"test-input-alpha");
let hash2 = fnv1a_hash(b"test-input-alpha");
assert_eq!(hash1, hash2);
}

#[test]
fn test_fnv1a_hash_differs_for_different_inputs() {
let h1 = fnv1a_hash(b"hello-world");
let h2 = fnv1a_hash(b"goodbye-world");
assert_ne!(h1, h2);
}

#[test]
fn test_fnv1a_hash_empty_returns_offset_basis() {
assert_eq!(fnv1a_hash(b""), 0xcbf29ce484222325);
}
}
125 changes: 125 additions & 0 deletions src/generators/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,4 +348,129 @@ mod tests {
assert!(result.value.is_nan());
assert_eq!(result.name, "test");
}

#[test]
fn test_from_ciborium_null() {
let hegel = HegelValue::from(ciborium::Value::Null);
assert!(matches!(hegel, HegelValue::Null));
}

#[test]
fn test_from_ciborium_bytes() {
let hegel = HegelValue::from(ciborium::Value::Bytes(vec![0xCA, 0xFE]));
if let HegelValue::Array(arr) = hegel {
assert_eq!(arr.len(), 2);
} else {
panic!("expected Array");
}
}

#[test]
fn test_from_ciborium_map() {
let cbor = ciborium::Value::Map(vec![
(
ciborium::Value::Text("key-alpha".into()),
ciborium::Value::Integer(42.into()),
),
(
ciborium::Value::Integer(99.into()),
ciborium::Value::Text("non-text-key".into()),
),
]);
let hegel = HegelValue::from(cbor);
if let HegelValue::Object(map) = hegel {
assert_eq!(map.len(), 2);
assert!(map.contains_key("key-alpha"));
} else {
panic!("expected Object");
}
}

#[test]
fn test_from_ciborium_positive_bignum_tag() {
// Tag 2 = positive bignum: value 256 encoded as 2 bytes
let cbor = ciborium::Value::Tag(2, Box::new(ciborium::Value::Bytes(vec![0x01, 0x00])));
let hegel = HegelValue::from(cbor);
if let HegelValue::BigInt(s) = hegel {
assert_eq!(s, "256");
} else {
panic!("expected BigInt");
}
}

#[test]
fn test_from_ciborium_negative_bignum_tag() {
// Tag 3 = negative bignum: value is -1 - n, where n = 255
let cbor = ciborium::Value::Tag(3, Box::new(ciborium::Value::Bytes(vec![0xFF])));
let hegel = HegelValue::from(cbor);
if let HegelValue::BigInt(s) = hegel {
assert_eq!(s, "-256");
} else {
panic!("expected BigInt");
}
}

#[test]
#[should_panic(expected = "Expected Bytes inside bignum tag 2")]
fn test_from_ciborium_positive_bignum_non_bytes_panics() {
let cbor = ciborium::Value::Tag(2, Box::new(ciborium::Value::Text("bad".into())));
let _ = HegelValue::from(cbor);
}

#[test]
#[should_panic(expected = "Expected Bytes inside bignum tag 3")]
fn test_from_ciborium_negative_bignum_non_bytes_panics() {
let cbor = ciborium::Value::Tag(3, Box::new(ciborium::Value::Text("bad".into())));
let _ = HegelValue::from(cbor);
}

#[test]
#[should_panic(expected = "Unexpected CBOR tag 99")]
fn test_from_ciborium_unknown_tag_panics() {
let cbor = ciborium::Value::Tag(99, Box::new(ciborium::Value::Null));
let _ = HegelValue::from(cbor);
}

#[test]
fn test_hegel_value_error_display() {
let err = HegelValueError("test-error-message".to_string());
assert_eq!(format!("{err}"), "test-error-message");
// Also test the Error trait
let _: &dyn std::error::Error = &err;
}

#[test]
fn test_hegel_value_error_custom() {
let err = <HegelValueError as serde::de::Error>::custom("custom-err-42");
assert_eq!(format!("{err}"), "custom-err-42");
}

#[test]
fn test_deserialize_null_as_unit() {
from_hegel_value::<()>(HegelValue::Null).unwrap();
}

#[test]
fn test_deserialize_null_as_option_none() {
let result: Option<i32> = from_hegel_value(HegelValue::Null).unwrap();
assert_eq!(result, None);
}

#[test]
fn test_deserialize_value_as_option_some() {
let result: Option<i32> = from_hegel_value(HegelValue::Number(42.0)).unwrap();
assert_eq!(result, Some(42));
}

#[test]
fn test_deserialize_invalid_bigint() {
let result: Result<i128, _> = from_hegel_value(HegelValue::BigInt("not-a-number".into()));
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("invalid big integer")
);
}
}
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,8 @@ pub use hegel_macros::test;
#[doc(hidden)]
pub use runner::hegel;
pub use runner::{HealthCheck, Hegel, Settings, Verbosity};

/// Mutex for serializing tests that modify environment variables.
/// Used across runner, antithesis, and integration test modules to prevent races.
#[doc(hidden)]
pub static ENV_TEST_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
Loading