use qubit_budget::MeasuredBudgetError;
use qubit_budget::ResourceLimit;
use qubit_budget::StructureLimits;
use qubit_budget::json::JsonMeasurement;
use qubit_budget::json::JsonResource;
use qubit_budget::json::JsonValueLimits;
#[test]
fn test_has_limits_reports_direct_and_nested_dimensions() {
assert!(!JsonValueLimits::<JsonResource, usize>::new().has_limits());
assert!(
JsonValueLimits::<JsonResource, usize>::builder()
.max_string_bytes(1)
.build()
.has_limits()
);
assert!(
JsonValueLimits::<JsonResource, usize>::builder()
.max_number_bytes(1)
.build()
.has_limits()
);
assert!(
JsonValueLimits::<JsonResource, usize>::builder()
.max_payload_bytes(1)
.build()
.has_limits()
);
assert!(
JsonValueLimits::<JsonResource, usize>::builder()
.max_depth(1)
.build()
.has_limits()
);
}
#[test]
fn test_default_uses_usize_quantity() {
let _: JsonValueLimits = JsonValueLimits::default();
}
#[test]
fn test_standard_builder_supports_u64_quantity() {
let limits = JsonValueLimits::<JsonResource, u64>::builder()
.max_depth(1_u64)
.max_nodes(2_u64)
.max_sequence_items(3_u64)
.max_map_entries(4_u64)
.max_key_bytes(5_u64)
.max_string_bytes(6_u64)
.max_number_bytes(7_u64)
.max_payload_bytes(8_u64)
.build();
assert_eq!(limits.max_depth(), Some(1_u64));
assert_eq!(limits.max_payload_bytes(), Some(8_u64));
let _ = limits.budget();
}
#[test]
fn test_standard_builder_configures_all_value_dimensions() {
let limits = JsonValueLimits::<JsonResource, usize>::builder()
.max_depth(1)
.max_nodes(2)
.max_sequence_items(3)
.max_map_entries(4)
.max_key_bytes(5)
.max_string_bytes(6)
.max_number_bytes(7)
.max_payload_bytes(8)
.build();
assert_eq!(limits.max_depth(), Some(1));
assert_eq!(limits.max_nodes(), Some(2));
assert_eq!(limits.max_sequence_items(), Some(3));
assert_eq!(limits.max_map_entries(), Some(4));
assert_eq!(limits.max_key_bytes(), Some(5));
assert_eq!(limits.max_string_bytes(), Some(6));
assert_eq!(limits.max_number_bytes(), Some(7));
assert_eq!(limits.max_payload_bytes(), Some(8));
assert_eq!(
limits.string_bytes_limit().expect("string limit").resource(),
&JsonResource::StringBytes,
);
}
#[test]
fn test_standard_builder_creates_budget() {
let mut budget = JsonValueLimits::<JsonResource, usize>::builder()
.max_nodes(1)
.build()
.budget();
let mut transaction = budget.transaction();
transaction
.try_admit(JsonMeasurement::Null { depth: 0 })
.expect("one node fits");
transaction.commit().expect("transaction commits");
assert_eq!(budget.used_nodes(), Some(1));
}
#[test]
fn test_structure_limits_expresses_borrowing_and_ownership() {
let limits = JsonValueLimits::<JsonResource, usize>::builder().max_depth(4).build();
let _: &StructureLimits<JsonResource, usize> = limits.structure_limits();
assert_eq!(limits.structure_limits().max_depth(), Some(4));
assert_eq!(limits.into_structure_limits().max_depth(), Some(4));
}
#[test]
fn test_new_value_limits_report_unconfigured_maxima() {
let limits = JsonValueLimits::<JsonResource, usize>::new();
assert_eq!(limits.max_depth(), None);
assert_eq!(limits.max_string_bytes(), None);
assert_eq!(limits.max_number_bytes(), None);
assert_eq!(limits.max_payload_bytes(), None);
}
#[test]
fn test_custom_resources_remain_attached_to_value_limits() {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Resource {
String,
Number,
Payload,
}
let limits = JsonValueLimits::<Resource, u8>::builder()
.string_bytes_limit(ResourceLimit::new(Resource::String, 3))
.number_bytes_limit(ResourceLimit::new(Resource::Number, 4))
.payload_bytes_limit(ResourceLimit::new(Resource::Payload, 5))
.build();
assert_eq!(
limits.string_bytes_limit().expect("string limit").resource(),
&Resource::String,
);
assert_eq!(
limits.number_bytes_limit().expect("number limit").resource(),
&Resource::Number,
);
assert_eq!(
limits.payload_bytes_limit().expect("payload limit").resource(),
&Resource::Payload,
);
}
#[test]
fn test_custom_resources_create_value_budget() {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Resource {
Nodes,
}
let mut budget = JsonValueLimits::<Resource, u8>::builder()
.structure_limits(
StructureLimits::builder()
.nodes_limit(ResourceLimit::new(Resource::Nodes, 1))
.build(),
)
.build()
.budget();
let mut transaction = budget.transaction();
transaction
.try_admit(JsonMeasurement::Null { depth: 0 })
.expect("one node fits");
transaction.commit().expect("transaction commits");
assert_eq!(budget.used_nodes(), Some(1));
}
#[test]
fn test_check_point_array_measurement_rejects_items_without_mutable_budget() {
let limits = JsonValueLimits::<JsonResource, usize>::builder()
.max_sequence_items(1)
.build();
let error = limits
.check_point(JsonMeasurement::Array { depth: 1, items: 2 })
.expect_err("two items must exceed the point limit");
assert_eq!(error.resource(), &JsonResource::SequenceItems);
}
#[test]
fn test_unconfigured_payload_skips_unrepresentable_conversion() {
let limits = JsonValueLimits::<JsonResource, u8>::builder().build();
limits
.check_point(JsonMeasurement::String {
depth: usize::MAX,
bytes: usize::MAX,
})
.expect("unconfigured dimensions must not convert native values");
}
#[test]
fn test_check_point_rejects_each_json_measurement_variant_at_point_limit() {
let limits = JsonValueLimits::<JsonResource, usize>::builder()
.max_depth(1)
.max_sequence_items(1)
.max_map_entries(1)
.max_key_bytes(1)
.max_string_bytes(1)
.max_number_bytes(1)
.build();
let measurements = [
(JsonMeasurement::Null { depth: 2 }, JsonResource::Depth),
(JsonMeasurement::Boolean { depth: 2 }, JsonResource::Depth),
(
JsonMeasurement::String { depth: 1, bytes: 2 },
JsonResource::StringBytes,
),
(
JsonMeasurement::Number { depth: 1, bytes: 2 },
JsonResource::NumberBytes,
),
(
JsonMeasurement::Array { depth: 1, items: 2 },
JsonResource::SequenceItems,
),
(
JsonMeasurement::Object { depth: 1, entries: 2 },
JsonResource::MapEntries,
),
(JsonMeasurement::Key { bytes: 2 }, JsonResource::KeyBytes),
];
for (measurement, resource) in measurements {
let error = limits
.check_point(measurement)
.expect_err("each measured dimension exceeds its point limit");
assert!(matches!(error, MeasuredBudgetError::Budget(_)));
assert_eq!(error.resource(), &resource);
}
}
#[test]
fn test_check_point_prioritizes_conversion_before_depth_and_point_limits() {
let limits = JsonValueLimits::<JsonResource, u8>::builder()
.structure_limits(
StructureLimits::<JsonResource, u8>::builder()
.depth_limit(ResourceLimit::new(JsonResource::Depth, u8::MAX))
.build(),
)
.string_bytes_limit(ResourceLimit::new(JsonResource::StringBytes, u8::MAX))
.build();
let error = limits
.check_point(JsonMeasurement::String {
depth: usize::from(u8::MAX) + 1,
bytes: usize::from(u8::MAX) + 1,
})
.expect_err("depth conversion must reject before point checks");
assert!(matches!(
error,
MeasuredBudgetError::Quantity {
resource: JsonResource::Depth,
..
}
));
}
#[test]
fn test_check_prioritizes_depth_before_variant_point_limit() {
let limits = JsonValueLimits::<JsonResource, usize>::builder()
.max_depth(1)
.max_string_bytes(1)
.build();
let error = limits
.check_point(JsonMeasurement::String { depth: 2, bytes: 2 })
.expect_err("depth must reject before string bytes");
assert_eq!(error.resource(), &JsonResource::Depth);
}
#[test]
fn test_check_payload_only_conversion_reports_payload_resource() {
let limits = JsonValueLimits::<JsonResource, u8>::builder()
.payload_bytes_limit(ResourceLimit::new(JsonResource::PayloadBytes, u8::MAX))
.build();
let error = limits
.check_point(JsonMeasurement::String {
depth: 0,
bytes: usize::from(u8::MAX) + 1,
})
.expect_err("configured payload conversion must reject oversized bytes");
assert!(matches!(
error,
MeasuredBudgetError::Quantity {
resource: JsonResource::PayloadBytes,
..
}
));
}
#[test]
fn test_check_prefers_point_resource_when_payload_limit_is_also_configured() {
let limits = JsonValueLimits::<JsonResource, u8>::builder()
.string_bytes_limit(ResourceLimit::new(JsonResource::StringBytes, u8::MAX))
.payload_bytes_limit(ResourceLimit::new(JsonResource::PayloadBytes, u8::MAX))
.build();
let error = limits
.check_point(JsonMeasurement::String {
depth: 0,
bytes: usize::from(u8::MAX) + 1,
})
.expect_err("point resource must identify the conversion failure");
assert!(matches!(
error,
MeasuredBudgetError::Quantity {
resource: JsonResource::StringBytes,
..
}
));
}
#[test]
fn test_check_conversion_failures_report_each_point_resource() {
let limits = JsonValueLimits::<JsonResource, u8>::builder()
.structure_limits(
StructureLimits::<JsonResource, u8>::builder()
.sequence_items_limit(ResourceLimit::new(JsonResource::SequenceItems, u8::MAX))
.map_entries_limit(ResourceLimit::new(JsonResource::MapEntries, u8::MAX))
.key_bytes_limit(ResourceLimit::new(JsonResource::KeyBytes, u8::MAX))
.build(),
)
.number_bytes_limit(ResourceLimit::new(JsonResource::NumberBytes, u8::MAX))
.build();
let overflow = usize::from(u8::MAX) + 1;
let measurements = [
(
JsonMeasurement::Number {
depth: 0,
bytes: overflow,
},
JsonResource::NumberBytes,
),
(
JsonMeasurement::Array {
depth: 0,
items: overflow,
},
JsonResource::SequenceItems,
),
(
JsonMeasurement::Object {
depth: 0,
entries: overflow,
},
JsonResource::MapEntries,
),
(JsonMeasurement::Key { bytes: overflow }, JsonResource::KeyBytes),
];
for (measurement, resource) in measurements {
let error = limits
.check_point(measurement)
.expect_err("configured native measurement must fit u8");
assert!(matches!(
error,
MeasuredBudgetError::Quantity {
resource: actual,
..
} if actual == resource
));
}
}