use std::io;
use std::io::Write;
use std::panic::AssertUnwindSafe;
use std::panic::catch_unwind;
use qubit_budget::ResourceBudget;
use qubit_budget::ResourceLimit;
use qubit_budget::StructureLimits;
use qubit_budget::json::JsonEncodeLimits;
use qubit_budget::json::JsonEncodeSession;
use qubit_budget::json::JsonMeasurement;
use qubit_budget::json::JsonResource;
use qubit_budget::json::JsonValueBudget;
use qubit_budget::json::JsonValueLimits;
use serde::Serialize;
use serde::Serializer;
use serde::ser::Error as SerError;
use serde::ser::SerializeSeq;
use crate::encode::json_encode_test_support::encode;
use crate::encode::json_encode_test_support::write_buffered;
use crate::encode::json_encode_test_support::write_incremental;
struct FailsAfterPrefix;
impl Serialize for FailsAfterPrefix {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut sequence = serializer.serialize_seq(Some(2))?;
sequence.serialize_element(&1_u8)?;
Err(SerError::custom("intentional streaming failure"))
}
}
struct PanicsAfterPrefix;
impl Serialize for PanicsAfterPrefix {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut sequence = serializer.serialize_seq(Some(2))?;
sequence.serialize_element(&1_u8)?;
panic!("intentional streaming panic");
}
}
struct PrefixWriter {
accepted: Vec<u8>,
maximum: usize,
}
impl Write for PrefixWriter {
fn write(&mut self, input: &[u8]) -> io::Result<usize> {
if self.accepted.len() == self.maximum {
return Err(io::Error::other("intentional writer failure"));
}
let accepted = (self.maximum - self.accepted.len()).min(input.len());
self.accepted.extend_from_slice(&input[..accepted]);
Ok(accepted)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[test]
fn test_encode_session_exposes_only_output_resource() {
let encode = JsonEncodeSession::from_limits(
JsonEncodeLimits::<JsonResource, usize>::builder()
.output_bytes_limit(ResourceLimit::new(JsonResource::OutputBytes, 8))
.build(),
);
assert_eq!(encode.max_output_bytes(), Some(8));
}
#[test]
fn test_encode_attempt_consumes_output_bytes_atomically() {
let mut session = JsonEncodeSession::from_limits(
JsonEncodeLimits::<JsonResource, usize>::builder()
.output_bytes_limit(ResourceLimit::new(JsonResource::OutputBytes, 3))
.build(),
);
let mut attempt = session.begin_value();
attempt.try_consume_output_bytes(3).expect("exact output fits");
let error = attempt
.try_consume_output_bytes(1)
.expect_err("output budget is exhausted");
assert_eq!(error.resource(), &JsonResource::OutputBytes);
}
#[test]
fn test_encode_attempt_preserves_embedded_value_limits() {
let value_limits = JsonValueLimits::<JsonResource, usize>::builder()
.string_bytes_limit(ResourceLimit::new(JsonResource::StringBytes, 2))
.payload_bytes_limit(ResourceLimit::new(JsonResource::PayloadBytes, 3))
.structure_limits(StructureLimits::builder().nodes_limit(ResourceLimit::new(JsonResource::Nodes, 2)))
.build();
let mut session = JsonEncodeSession::from_limits(
JsonEncodeLimits::<JsonResource, usize>::builder()
.value_limits(value_limits)
.build(),
);
let mut attempt = session.begin_value();
attempt
.try_admit(JsonMeasurement::String { depth: 1, bytes: 2 })
.expect("exact string limit fits");
let string_error = attempt
.try_admit(JsonMeasurement::String { depth: 1, bytes: 3 })
.expect_err("overlong string poisons the attempt");
assert_eq!(string_error.resource(), &JsonResource::StringBytes);
let commit_error = attempt.commit().expect_err("poisoned string attempt cannot commit");
assert_eq!(commit_error.resource(), string_error.resource());
let mut attempt = session.begin_value();
attempt
.try_admit(JsonMeasurement::String { depth: 1, bytes: 2 })
.expect("string payload fits");
attempt
.try_admit(JsonMeasurement::Key { bytes: 1 })
.expect("exact payload limit fits");
let payload_error = attempt
.try_admit(JsonMeasurement::Key { bytes: 1 })
.expect_err("exhausted payload poisons the attempt");
assert_eq!(payload_error.resource(), &JsonResource::PayloadBytes);
let commit_error = attempt.commit().expect_err("poisoned payload attempt cannot commit");
assert_eq!(commit_error.resource(), payload_error.resource());
let mut attempt = session.begin_value();
attempt
.try_admit(JsonMeasurement::Null { depth: 1 })
.expect("first node fits");
attempt
.try_admit(JsonMeasurement::Null { depth: 1 })
.expect("exact node limit fits");
let node_error = attempt
.try_admit(JsonMeasurement::Null { depth: 1 })
.expect_err("exhausted node budget poisons the attempt");
assert_eq!(node_error.resource(), &JsonResource::Nodes);
let commit_error = attempt.commit().expect_err("poisoned node attempt cannot commit");
assert_eq!(commit_error.resource(), node_error.resource());
assert_eq!(session.value_budget().used_nodes(), Some(0));
assert_eq!(session.value_budget().used_payload_bytes(), Some(0));
}
#[test]
fn test_encode_session_borrows_output_and_value_budgets() {
let mut output = ResourceBudget::new(JsonResource::OutputBytes, 32);
let mut value = JsonValueBudget::new(
JsonValueLimits::<JsonResource, usize>::builder()
.structure_limits(StructureLimits::builder().nodes_limit(ResourceLimit::new(JsonResource::Nodes, 16)))
.build(),
);
let mut session = JsonEncodeSession::borrowing_output(&mut output, &mut value);
let encoded = encode(&serde_json::json!({"name": "qubit"}), &mut session)
.expect("borrowed budgets should support online encoding");
assert_eq!(encoded, br#"{"name":"qubit"}"#);
assert_eq!(output.used(), encoded.len());
assert!(value.used_nodes().expect("nodes limit should be configured") > 0);
}
#[test]
fn test_encode_stream_failure_rolls_back_output_and_value() {
let mut session = JsonEncodeSession::from_limits(
JsonEncodeLimits::<JsonResource, usize>::builder()
.max_output_bytes(64)
.max_nodes(2)
.build(),
);
assert!(encode(&FailsAfterPrefix, &mut session).is_err());
assert_eq!(session.output_budget().expect("output budget").used(), 0,);
assert_eq!(session.value_budget().used_nodes(), Some(0));
}
#[test]
fn test_buffered_writer_failure_keeps_prefix_and_rolls_back_value() {
let mut session = JsonEncodeSession::from_limits(
JsonEncodeLimits::<JsonResource, usize>::builder()
.max_output_bytes(64)
.max_nodes(3)
.build(),
);
let mut writer = PrefixWriter {
accepted: Vec::new(),
maximum: 2,
};
assert!(write_buffered(&mut writer, &[1_u8, 2_u8], &mut session).is_err());
assert_eq!(writer.accepted.len(), 2);
assert_eq!(
session.output_budget().expect("output budget").used(),
writer.accepted.len(),
);
assert_eq!(session.value_budget().used_nodes(), Some(0));
}
#[test]
fn test_incremental_stream_failure_keeps_prefix_and_rolls_back_value() {
let mut session = JsonEncodeSession::from_limits(
JsonEncodeLimits::<JsonResource, usize>::builder()
.max_output_bytes(64)
.max_nodes(2)
.build(),
);
let mut writer = Vec::new();
assert!(write_incremental(&mut writer, &FailsAfterPrefix, &mut session).is_err());
assert!(!writer.is_empty());
assert_eq!(session.output_budget().expect("output budget").used(), writer.len(),);
assert_eq!(session.value_budget().used_nodes(), Some(0));
}
#[test]
fn test_incremental_panic_keeps_prefix_and_reuses_value_capacity() {
let mut session = JsonEncodeSession::from_limits(
JsonEncodeLimits::<JsonResource, usize>::builder()
.max_output_bytes(64)
.max_nodes(2)
.build(),
);
let mut writer = Vec::new();
let result = catch_unwind(AssertUnwindSafe(|| {
write_incremental(&mut writer, &PanicsAfterPrefix, &mut session)
}));
assert!(result.is_err());
assert!(!writer.is_empty());
assert_eq!(session.output_budget().expect("output budget").used(), writer.len(),);
assert_eq!(session.value_budget().used_nodes(), Some(0));
encode(&true, &mut session).expect("panic must leave value capacity for the next encode");
assert_eq!(session.value_budget().used_nodes(), Some(1));
}