use std::panic::AssertUnwindSafe;
use qubit_budget::ResourceBudget;
use qubit_budget::json::JsonDecodeLimits;
use qubit_budget::json::JsonDecodeSession;
use qubit_budget::json::JsonMeasurement;
use qubit_budget::json::JsonResource;
use qubit_budget::json::JsonValueLimits;
#[test]
fn test_decode_attempt_drop_keeps_input_and_rolls_back_value() {
let mut session = JsonDecodeSession::from_limits(
JsonDecodeLimits::<JsonResource, usize>::builder()
.max_input_bytes(8)
.max_normalized_input_bytes(7)
.max_nodes(2)
.build(),
);
{
let mut attempt = session.begin_value();
attempt.try_consume_input_bytes(3).expect("input fits");
attempt
.try_consume_normalized_input_bytes(2)
.expect("normalized input fits");
attempt
.try_admit(JsonMeasurement::Null { depth: 1 })
.expect("value fits");
}
assert_eq!(session.input_budget().expect("configured input").used(), 3);
assert_eq!(
session
.normalized_input_budget()
.expect("configured normalized input")
.used(),
2
);
assert_eq!(session.value_budget().used_nodes(), Some(0));
}
#[test]
fn test_decode_attempt_poisoned_commit_returns_error() {
let mut session =
JsonDecodeSession::from_limits(JsonDecodeLimits::<JsonResource, usize>::builder().max_nodes(1).build());
let mut attempt = session.begin_value();
attempt
.try_admit(JsonMeasurement::Null { depth: 1 })
.expect("first value fits");
let first_error = attempt
.try_admit(JsonMeasurement::Null { depth: 1 })
.expect_err("second value poisons the attempt");
let commit_error = attempt.commit().expect_err("poisoned decode attempt cannot commit");
assert_eq!(commit_error.resource(), first_error.resource());
assert_eq!(session.value_budget().used_nodes(), Some(0));
}
#[test]
fn test_decode_session_begin_value_commits_and_reuses_value_budget() {
let mut session =
JsonDecodeSession::from_limits(JsonDecodeLimits::<JsonResource, usize>::builder().max_nodes(2).build());
let mut first = session.begin_value();
first
.try_admit(JsonMeasurement::Null { depth: 1 })
.expect("first value fits");
assert_eq!(first.used_nodes(), Some(1));
assert_eq!(first.remaining_nodes(), Some(1));
first.commit().expect("first attempt commits");
let mut second = session.begin_value();
second
.try_admit(JsonMeasurement::Null { depth: 1 })
.expect("second value fits");
second.commit().expect("second attempt commits");
let mut rejected = session.begin_value();
assert!(rejected.try_admit(JsonMeasurement::Null { depth: 1 }).is_err());
assert_eq!(session.value_budget().used_nodes(), Some(2));
}
#[test]
fn test_decode_attempt_value_transaction_mut_exposes_working_state() {
let mut session = JsonDecodeSession::from_limits(
JsonDecodeLimits::<JsonResource, usize>::builder()
.max_payload_bytes(4)
.build(),
);
let mut attempt = session.begin_value();
attempt
.value_transaction_mut()
.try_admit(JsonMeasurement::String { depth: 1, bytes: 3 })
.expect("string fits");
assert_eq!(attempt.used_payload_bytes(), Some(3));
assert_eq!(attempt.remaining_payload_bytes(), Some(1));
attempt.commit().expect("attempt commits");
assert_eq!(session.value_budget().used_payload_bytes(), Some(3));
}
#[test]
fn test_decode_session_borrowing_value_reuses_committed_budget() {
let mut value = JsonValueLimits::<JsonResource, usize>::builder()
.max_nodes(2)
.build()
.budget();
{
let mut session = JsonDecodeSession::borrowing_value(&mut value);
let mut attempt = session.begin_value();
attempt
.try_consume_input_bytes(99)
.expect("unconfigured input is ignored");
attempt
.try_admit(JsonMeasurement::Null { depth: 1 })
.expect("value fits");
attempt.commit().expect("attempt commits");
}
assert_eq!(value.used_nodes(), Some(1));
}
#[test]
fn test_decode_session_accessors_report_configured_budgets() {
let session = JsonDecodeSession::from_limits(
JsonDecodeLimits::<JsonResource, usize>::builder()
.max_input_bytes(8)
.max_normalized_input_bytes(7)
.build(),
);
assert_eq!(session.max_input_bytes(), Some(8));
assert_eq!(session.max_normalized_input_bytes(), Some(7));
assert!(session.input_budget().is_some());
assert!(session.normalized_input_budget().is_some());
assert!(session.value_budget().used_nodes().is_none());
let mut value = JsonValueLimits::<JsonResource, usize>::new().budget();
let session = JsonDecodeSession::borrowing_value(&mut value);
assert_eq!(session.max_input_bytes(), None);
assert_eq!(session.max_normalized_input_bytes(), None);
assert!(session.input_budget().is_none());
assert!(session.normalized_input_budget().is_none());
}
#[test]
fn test_decode_session_borrowing_input_keeps_charge_after_attempt_drop() {
let mut input = ResourceBudget::new(JsonResource::InputBytes, 3_usize);
let mut value = JsonValueLimits::<JsonResource, usize>::builder()
.max_nodes(2)
.build()
.budget();
{
let mut session = JsonDecodeSession::borrowing_input(&mut input, &mut value);
let mut attempt = session.begin_value();
attempt.try_consume_input_bytes(3).expect("input fits");
attempt
.try_admit(JsonMeasurement::Null { depth: 1 })
.expect("value fits");
}
assert_eq!(input.used(), 3);
assert_eq!(value.used_nodes(), Some(0));
}
#[test]
fn test_decode_session_borrowing_all_commits_io_and_value() {
let mut input = ResourceBudget::new(JsonResource::InputBytes, 3_usize);
let mut normalized = ResourceBudget::new(JsonResource::NormalizedInputBytes, 4_usize);
let mut value = JsonValueLimits::<JsonResource, usize>::builder()
.max_nodes(2)
.build()
.budget();
{
let mut session = JsonDecodeSession::borrowing_all(&mut input, &mut normalized, &mut value);
let mut attempt = session.begin_value();
attempt.try_consume_input_bytes(3).expect("input fits");
attempt
.try_consume_normalized_input_bytes(4)
.expect("normalized input fits");
attempt
.try_admit(JsonMeasurement::Null { depth: 1 })
.expect("value fits");
attempt.commit().expect("attempt commits");
}
assert_eq!(input.used(), 3);
assert_eq!(normalized.used(), 4);
assert_eq!(value.used_nodes(), Some(1));
}
#[test]
fn test_decode_attempt_panic_keeps_input_and_rolls_back_value() {
let mut session = JsonDecodeSession::from_limits(
JsonDecodeLimits::<JsonResource, usize>::builder()
.max_input_bytes(3)
.max_nodes(1)
.build(),
);
let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
let mut attempt = session.begin_value();
attempt.try_consume_input_bytes(3).expect("input fits");
attempt
.try_admit(JsonMeasurement::Null { depth: 1 })
.expect("value fits");
panic!("abort decode after accounting");
}));
assert!(result.is_err());
assert_eq!(session.input_budget().expect("configured input").used(), 3);
assert_eq!(session.value_budget().used_nodes(), Some(0));
}