use crate::control::{
AssumeFailed, InternalError, InvalidArgument, LoopDone, StopTest, hegel_internal_assert,
hegel_internal_error, raise_control,
};
use crate::ffi::CTestCase;
use crate::generators::Generator;
use crate::runner::Mode;
use ciborium::Value;
use parking_lot::Mutex;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
use std::sync::Arc;
use crate::generators::value;
#[diagnostic::on_unimplemented(
message = "The first parameter in a #[composite] generator must have type TestCase.",
label = "This type does not match `TestCase`."
)]
pub trait __IsTestCase {}
impl __IsTestCase for TestCase {}
pub fn __assert_is_test_case<T: __IsTestCase>() {}
#[track_caller]
pub(crate) fn raise_invalid_argument(message: std::fmt::Arguments<'_>) -> ! {
if crate::control::currently_in_test_context() {
raise_control(InvalidArgument(message.to_string()));
} else {
panic!("{message}");
}
}
macro_rules! invalid_argument {
($($arg:tt)*) => {
$crate::test_case::raise_invalid_argument(::std::format_args!($($arg)*))
};
}
pub(crate) use invalid_argument;
#[track_caller]
pub(crate) fn raise_for_rc(rc: hegel_c::hegel_result_t) -> ! {
use hegel_c::hegel_result_t::*;
match rc {
HEGEL_E_STOP_TEST => raise_control(StopTest),
HEGEL_E_ASSUME => raise_control(AssumeFailed), HEGEL_E_INVALID_ARG => invalid_argument!("{}", crate::ffi::last_error_string()),
HEGEL_E_ALREADY_COMPLETE => panic!(
"this test case has already finished; was the TestCase moved to a \
thread that outlived the test? Join any thread that draws before \
the test returns."
),
other => hegel_internal_error!(
"libhegel returned unexpected code {}: {}",
other as i32,
crate::ffi::last_error_string()
),
}
}
pub(crate) struct TestCaseGlobalData {
mode: Mode,
emit: bool,
draw_state: Mutex<DrawState>,
}
pub(crate) struct DrawState {
named_draw_counts: HashMap<String, usize>,
named_draw_repeatable: HashMap<String, bool>,
allocated_display_names: HashSet<String>,
}
#[derive(Clone)]
pub(crate) struct TestCaseLocalData {
span_depth: usize,
indent: usize,
on_draw: OutputSink,
}
pub struct TestCase {
global: Arc<TestCaseGlobalData>,
local: RefCell<TestCaseLocalData>,
handle: Arc<CTestCase>,
}
impl Clone for TestCase {
fn clone(&self) -> Self {
TestCase {
global: self.global.clone(),
local: RefCell::new(self.local.borrow().clone()),
handle: Arc::new(self.handle.clone_handle()),
}
}
}
impl std::fmt::Debug for TestCase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TestCase").finish_non_exhaustive()
}
}
pub(crate) type OutputSink = Arc<dyn Fn(&str) + Send + Sync>;
thread_local! {
static OUTPUT_OVERRIDE: RefCell<Option<OutputSink>> = const { RefCell::new(None) };
}
#[doc(hidden)]
pub fn with_output_override<R>(sink: OutputSink, f: impl FnOnce() -> R) -> R {
let prev = OUTPUT_OVERRIDE.with(|cell| cell.borrow_mut().replace(sink));
let result = f();
OUTPUT_OVERRIDE.with(|cell| *cell.borrow_mut() = prev);
result
}
pub(crate) fn current_output_sink() -> Option<OutputSink> {
OUTPUT_OVERRIDE.with(|cell| cell.borrow().clone())
}
pub(crate) fn emit_verbose_line(msg: &str) {
if let Some(sink) = current_output_sink() {
sink(msg);
} else {
eprintln!("{}", msg);
}
}
impl TestCase {
pub(crate) fn new(handle: Arc<CTestCase>, emit: bool, mode: Mode) -> Self {
let override_sink = current_output_sink();
let on_draw: OutputSink = match override_sink {
Some(sink) if emit => sink,
_ if emit => Arc::new(|msg| eprintln!("{}", msg)),
_ => Arc::new(|_| {}),
};
TestCase {
global: Arc::new(TestCaseGlobalData {
mode,
emit,
draw_state: Mutex::new(DrawState {
named_draw_counts: HashMap::new(),
named_draw_repeatable: HashMap::new(),
allocated_display_names: HashSet::new(),
}),
}),
local: RefCell::new(TestCaseLocalData {
span_depth: 0,
indent: 0,
on_draw,
}),
handle,
}
}
pub(crate) fn mode(&self) -> Mode {
self.global.mode
}
pub(crate) fn with_draw_state<R>(&self, f: impl FnOnce(&mut DrawState) -> R) -> R {
let mut guard = self.global.draw_state.lock();
f(&mut guard)
}
pub fn draw<T: std::fmt::Debug>(&self, generator: impl Generator<T>) -> T {
self.__draw_named(generator, "draw", true)
}
pub fn __draw_named<T: std::fmt::Debug>(
&self,
generator: impl Generator<T>,
name: &str,
repeatable: bool,
) -> T {
let value = generator.do_draw(self);
if self.local.borrow().span_depth == 0 {
self.record_named_draw(&value, name, repeatable);
}
value
}
pub fn draw_silent<T>(&self, generator: impl Generator<T>) -> T {
generator.do_draw(self)
}
pub fn assume(&self, condition: bool) {
if !condition {
self.reject();
}
}
pub fn reject(&self) -> ! {
raise_control(AssumeFailed);
}
pub fn note(&self, message: &str) {
let local = self.local.borrow();
let indent = local.indent;
(local.on_draw)(&format!("{:indent$}{}", "", message, indent = indent));
}
pub fn target(&self, score: f64) {
self.target_labelled(score, "");
}
pub fn target_labelled(&self, score: f64, label: impl Into<String>) {
let label = label.into();
let outcome = self.with_ctc(|ctc| ctc.target(score, &label));
if let Err(rc) = outcome {
raise_for_rc(rc);
}
}
pub fn repeat<F: FnMut()>(&self, mut body: F) -> ! {
if self.global.mode == Mode::SingleTestCase {
self.repeat_single_test_case(&mut body);
}
self.repeat_property_test(&mut body);
}
fn repeat_single_test_case(&self, body: &mut dyn FnMut()) -> ! {
let mut iteration: u64 = 0;
loop {
iteration += 1;
self.note(&format!("// Repetition #{}", iteration));
let prev_indent = self.local.borrow().indent;
self.local.borrow_mut().indent = prev_indent + 2;
body();
self.local.borrow_mut().indent = prev_indent;
}
}
fn repeat_property_test(&self, body: &mut dyn FnMut()) -> ! {
use crate::generators::{booleans, integers};
const MAX_SAFE_MIN_SIZE: usize = 1 << 40;
let min_size = self.draw_silent(integers::<usize>().max_value(MAX_SAFE_MIN_SIZE));
let mut collection = Collection::new(self, min_size, None);
let mut iteration: u64 = 0;
while collection.more() {
iteration += 1;
self.note(&format!("// Repetition #{}", iteration));
let prev_indent = self.local.borrow().indent;
self.local.borrow_mut().indent = prev_indent + 2;
let result = catch_unwind(AssertUnwindSafe(&mut *body));
self.local.borrow_mut().indent = prev_indent;
match result {
Ok(()) => {}
Err(e) if e.downcast_ref::<AssumeFailed>().is_some() => {}
Err(e)
if e.downcast_ref::<StopTest>().is_some()
|| e.downcast_ref::<InvalidArgument>().is_some()
|| e.downcast_ref::<InternalError>().is_some() =>
{
resume_unwind(e);
}
Err(e) => {
self.draw_silent(booleans());
resume_unwind(e);
}
}
}
raise_control(LoopDone);
}
pub(crate) fn child(&self, extra_indent: usize) -> Self {
let local = self.local.borrow();
TestCase {
global: self.global.clone(),
local: RefCell::new(TestCaseLocalData {
span_depth: 0,
indent: local.indent + extra_indent,
on_draw: local.on_draw.clone(),
}),
handle: Arc::clone(&self.handle),
}
}
fn record_named_draw<T: std::fmt::Debug>(&self, value: &T, name: &str, repeatable: bool) {
let emit = self.global.emit;
let display_name = self.with_draw_state(|draw_state| {
match draw_state.named_draw_repeatable.get(name) {
Some(&prev) if prev != repeatable => {
hegel_internal_error!(
"__draw_named: name {:?} used with inconsistent repeatable flag \
(was {}, now {})",
name,
prev,
repeatable
);
}
Some(_) => {}
None => {
draw_state
.named_draw_repeatable
.insert(name.to_string(), repeatable);
}
}
let current_count = match draw_state.named_draw_counts.get_mut(name) {
Some(count) => {
*count += 1;
*count
}
None => {
draw_state.named_draw_counts.insert(name.to_string(), 1);
1
}
};
if !repeatable && current_count > 1 {
hegel_internal_error!(
"__draw_named: name {:?} used more than once but repeatable is false",
name
);
}
if !emit {
return None;
}
let display = if repeatable {
let mut candidate = current_count;
loop {
let name = format!("{}_{}", name, candidate);
if draw_state.allocated_display_names.insert(name.clone()) {
break name;
}
candidate += 1;
}
} else {
let name = name.to_string();
draw_state.allocated_display_names.insert(name.clone());
name
};
Some(display)
});
let Some(display_name) = display_name else {
return;
};
let local = self.local.borrow();
let indent = local.indent;
(local.on_draw)(&format!(
"{:indent$}let {} = {:?};",
"",
display_name,
value,
indent = indent
));
}
pub(crate) fn with_ctc<R>(&self, f: impl FnOnce(&CTestCase) -> R) -> R {
f(&self.handle)
}
#[doc(hidden)]
pub fn start_span(&self, label: u64) {
self.local.borrow_mut().span_depth += 1;
if let Err(rc) = self.with_ctc(|ctc| ctc.start_span(label)) {
let mut local = self.local.borrow_mut();
hegel_internal_assert!(local.span_depth > 0);
local.span_depth -= 1;
drop(local);
raise_for_rc(rc);
}
}
#[doc(hidden)]
pub fn stop_span(&self, discard: bool) {
{
let mut local = self.local.borrow_mut();
hegel_internal_assert!(local.span_depth > 0);
local.span_depth -= 1;
}
if let Err(rc) = self.with_ctc(|ctc| ctc.stop_span(discard)) {
raise_for_rc(rc);
}
}
}
#[doc(hidden)]
pub fn generate_raw(tc: &TestCase, schema: &Value) -> Value {
let mut schema_bytes = Vec::new();
ciborium::ser::into_writer(schema, &mut schema_bytes).unwrap_or_else(|e| {
hegel_internal_error!("failed to serialize schema: {}", e) });
let value_bytes = match tc.with_ctc(|ctc| ctc.generate(&schema_bytes)) {
Ok(bytes) => bytes,
Err(rc) => raise_for_rc(rc),
};
ciborium::de::from_reader(value_bytes.as_slice()).unwrap_or_else(|e| {
hegel_internal_error!("failed to deserialize value: {}", e) })
}
#[doc(hidden)]
pub fn generate_from_schema<T: serde::de::DeserializeOwned>(tc: &TestCase, schema: &Value) -> T {
deserialize_value(generate_raw(tc, schema))
}
pub fn deserialize_value<T: serde::de::DeserializeOwned>(raw: Value) -> T {
let hv = value::HegelValue::from(raw.clone());
value::from_hegel_value(hv).unwrap_or_else(|e| {
hegel_internal_error!("failed to deserialize value: {}\nValue: {:?}", e, raw); })
}
pub struct Collection<'a> {
tc: &'a TestCase,
min_size: usize,
max_size: Option<usize>,
handle: Option<i64>,
finished: bool,
}
impl<'a> Collection<'a> {
pub fn new(tc: &'a TestCase, min_size: usize, max_size: Option<usize>) -> Self {
Collection {
tc,
min_size,
max_size,
handle: None,
finished: false,
}
}
fn ensure_initialized(&mut self) -> i64 {
if self.handle.is_none() {
let result = self.tc.with_ctc(|ctc| {
ctc.new_collection(self.min_size as u64, self.max_size.map(|m| m as u64))
});
let id = match result {
Ok(id) => id,
Err(rc) => raise_for_rc(rc), };
self.handle = Some(id);
}
self.handle.unwrap()
}
pub fn more(&mut self) -> bool {
if self.finished {
return false; }
let handle = self.ensure_initialized();
let result = match self.tc.with_ctc(|ctc| ctc.collection_more(handle)) {
Ok(b) => b,
Err(rc) => {
self.finished = true;
raise_for_rc(rc);
}
};
if !result {
self.finished = true;
}
result
}
pub fn reject(&mut self, why: Option<&str>) {
if self.finished {
return;
}
let handle = self.ensure_initialized();
let _ = self.tc.with_ctc(|ctc| ctc.collection_reject(handle, why));
}
}
#[doc(hidden)]
pub mod labels {
pub const LIST: u64 = 1;
pub const LIST_ELEMENT: u64 = 2;
pub const SET: u64 = 3;
pub const SET_ELEMENT: u64 = 4;
pub const MAP: u64 = 5;
pub const MAP_ENTRY: u64 = 6;
pub const TUPLE: u64 = 7;
pub const ONE_OF: u64 = 8;
pub const OPTIONAL: u64 = 9;
pub const FIXED_DICT: u64 = 10;
pub const FLAT_MAP: u64 = 11;
pub const FILTER: u64 = 12;
pub const MAPPED: u64 = 13;
pub const SAMPLED_FROM: u64 = 14;
pub const ENUM_VARIANT: u64 = 15;
pub const FEATURE_FLAG: u64 = 16;
}
#[cfg(test)]
#[path = "../tests/embedded/test_case_tests.rs"]
mod tests;