use std::cell::Cell;
use cadmpeg_core::decode::{
u64_from_index, DecodeContext, DepthGuard, ResourceDimension, ResourceFailure, ResourceLimit,
WorkBudget,
};
use super::EvaluationFailure;
const INDEPENDENT_EVALUATION_DEPTH: usize = 256;
const MAX_MODEL_EVALUATION_WORK: usize = 1 << 24;
const MAX_MODEL_PASS_EVALUATION_WORK: usize = 1 << 30;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EvaluationRefusal {
WorkExhausted,
ResourceLimit(ResourceLimit),
}
impl From<ResourceLimit> for EvaluationRefusal {
fn from(limit: ResourceLimit) -> Self {
Self::ResourceLimit(limit)
}
}
impl EvaluationRefusal {
pub(super) fn absent<T>(self) -> Result<Option<T>, ResourceLimit> {
match self {
Self::WorkExhausted => Ok(None),
Self::ResourceLimit(limit) => Err(limit),
}
}
}
#[cfg(test)]
impl EvaluationRefusal {
pub(super) fn into_limit(self) -> ResourceLimit {
match self {
Self::ResourceLimit(limit) => limit,
Self::WorkExhausted => panic!("an evaluation without a work slice exhausted one"),
}
}
}
#[derive(Clone, Copy)]
pub enum EvaluationAdmission<'ctx, 'arena> {
Decode(&'ctx DecodeContext<'arena>),
Standard,
WorkSlice(EvaluationWorkSlice<'ctx, 'arena>),
}
#[derive(Clone, Copy)]
pub struct EvaluationWorkSlice<'ctx, 'arena> {
policy: WorkSlicePolicy<'ctx, 'arena>,
}
#[derive(Clone, Copy)]
enum WorkSlicePolicy<'ctx, 'arena> {
Independent(&'ctx WorkBudget),
Standalone(&'ctx WorkBudget),
Session {
context: &'ctx DecodeContext<'arena>,
work: &'ctx WorkBudget,
},
}
impl<'ctx, 'arena> From<&'ctx DecodeContext<'arena>> for EvaluationAdmission<'ctx, 'arena> {
fn from(context: &'ctx DecodeContext<'arena>) -> Self {
Self::Decode(context)
}
}
impl<'ctx, 'arena> EvaluationAdmission<'ctx, 'arena> {
pub(super) fn within_model<T, R>(
self,
run: impl for<'frame> FnOnce(
EvaluationAdmission<'frame, 'frame>,
) -> Result<T, EvaluationFailure<R>>,
) -> Result<T, EvaluationFailure<R>> {
let evaluate = |admission: EvaluationAdmission<'_, '_>| {
let scratch = super::decode::Scratch::new(admission);
scratch.unless_refused()?;
let result = run(admission);
let result = match admission.work_slice() {
Some(_) => super::ModelEvaluationDepthGuard::finish_sliced(admission, result),
None => result,
};
scratch.settle(result)
};
let work = WorkBudget::new(MAX_MODEL_EVALUATION_WORK);
match self {
Self::Decode(_) => self.within_work_slice(&work, evaluate),
Self::Standard => evaluate(EvaluationAdmission::WorkSlice(EvaluationWorkSlice {
policy: WorkSlicePolicy::Standalone(&work),
})),
Self::WorkSlice(_) => evaluate(self),
}
}
pub(super) fn independent_work_slice(self) -> Option<&'ctx WorkBudget> {
match self {
Self::WorkSlice(EvaluationWorkSlice {
policy: WorkSlicePolicy::Independent(work),
}) => Some(work),
_ => None,
}
}
pub(super) fn work_slice(self) -> Option<&'ctx WorkBudget> {
match self {
Self::WorkSlice(EvaluationWorkSlice {
policy:
WorkSlicePolicy::Independent(work)
| WorkSlicePolicy::Standalone(work)
| WorkSlicePolicy::Session { work, .. },
}) => Some(work),
Self::Decode(_) | Self::Standard => None,
}
}
pub(super) fn model_step<R>(self) -> Result<(), EvaluationFailure<R>> {
match self.work_slice() {
Some(work) if !work.charge() => Err(self.exhausted().into()),
_ => Ok(()),
}
}
pub fn within_work_slice<T, R>(
self,
parent: &WorkBudget,
run: impl for<'slice> FnOnce(
EvaluationAdmission<'slice, 'slice>,
) -> Result<T, EvaluationFailure<R>>,
) -> Result<T, EvaluationFailure<R>> {
match self.context() {
Some(context) => {
session_refusal(context)?;
let child = WorkBudget::new(parent.remaining());
let result = run(EvaluationAdmission::WorkSlice(EvaluationWorkSlice {
policy: WorkSlicePolicy::Session {
context,
work: &child,
},
}));
if parent.consume_child(&child).is_ok() && child.exhausted() {
parent.exhaust();
}
session_refusal(context)?;
result
}
None => {
let policy = match self {
Self::WorkSlice(EvaluationWorkSlice {
policy: WorkSlicePolicy::Standalone(_),
}) => WorkSlicePolicy::Standalone(parent),
_ => WorkSlicePolicy::Independent(parent),
};
run(EvaluationAdmission::WorkSlice(EvaluationWorkSlice {
policy,
}))
}
}
}
pub(super) fn context(self) -> Option<&'ctx DecodeContext<'arena>> {
match self {
Self::Decode(context) => Some(context),
Self::Standard => None,
Self::WorkSlice(EvaluationWorkSlice {
policy: WorkSlicePolicy::Session { context, .. },
}) => Some(context),
Self::WorkSlice(EvaluationWorkSlice {
policy: WorkSlicePolicy::Independent(_) | WorkSlicePolicy::Standalone(_),
}) => None,
}
}
pub(super) fn independent_cost<R>(
self,
cost: Option<usize>,
) -> Result<(), EvaluationFailure<R>> {
match self {
Self::WorkSlice(EvaluationWorkSlice {
policy: WorkSlicePolicy::Independent(work),
}) => cost
.is_some_and(|cost| work.charge_by(cost))
.then_some(())
.ok_or(EvaluationFailure::WorkExhausted),
_ => Ok(()),
}
}
pub(super) fn work(self, count: usize) -> Result<(), EvaluationRefusal> {
match self {
Self::WorkSlice(EvaluationWorkSlice {
policy: WorkSlicePolicy::Session { work, .. } | WorkSlicePolicy::Standalone(work),
}) if !work.charge_by(count) => Err(self.exhausted()),
_ => Ok(()),
}
}
pub(super) fn refusal(self) -> Option<EvaluationRefusal> {
let refusal = self
.context()
.and_then(DecodeContext::resource_refusal)
.map(EvaluationRefusal::ResourceLimit);
match self {
Self::WorkSlice(EvaluationWorkSlice {
policy: WorkSlicePolicy::Session { work, .. } | WorkSlicePolicy::Standalone(work),
}) if work.exhausted() => refusal.or(Some(EvaluationRefusal::WorkExhausted)),
_ => refusal,
}
}
fn exhausted(self) -> EvaluationRefusal {
self.context()
.and_then(DecodeContext::resource_refusal)
.map_or(
EvaluationRefusal::WorkExhausted,
EvaluationRefusal::ResourceLimit,
)
}
pub(super) fn enter<'scratch>(
self,
depth: &'scratch Cell<usize>,
) -> Result<EvaluationDepthGuard<'scratch>, ResourceLimit>
where
'ctx: 'scratch,
{
match self.context() {
Some(context) => context
.enter_nested_limit("geometry evaluation nesting")
.map(|guard| EvaluationDepthGuard::Decode { _guard: guard }),
None => {
if depth.get() >= INDEPENDENT_EVALUATION_DEPTH {
return Err(ResourceLimit {
dimension: ResourceDimension::RecursionDepth,
reason: ResourceFailure::BudgetExceeded,
limit: u64_from_index(INDEPENDENT_EVALUATION_DEPTH),
used: u64_from_index(depth.get()),
additional: 1,
operation: "independent geometry evaluation nesting",
});
}
depth.set(depth.get() + 1);
Ok(EvaluationDepthGuard::Standard { depth })
}
}
}
}
fn session_refusal<R>(context: &DecodeContext<'_>) -> Result<(), EvaluationFailure<R>> {
context
.resource_refusal()
.map_or(Ok(()), |limit| Err(EvaluationFailure::ResourceLimit(limit)))
}
pub(crate) struct ModelEvaluationPass {
allowance: WorkBudget,
capped: Cell<usize>,
starved: Cell<usize>,
}
impl ModelEvaluationPass {
pub(crate) fn new() -> Self {
Self::with_allowance(MAX_MODEL_PASS_EVALUATION_WORK)
}
fn with_allowance(allowance: usize) -> Self {
Self {
allowance: WorkBudget::new(allowance),
capped: Cell::new(0),
starved: Cell::new(0),
}
}
pub(crate) fn evaluate<T, R>(
&self,
admission: EvaluationAdmission<'_, '_>,
run: impl for<'slice> FnOnce(
EvaluationAdmission<'slice, 'slice>,
) -> Result<T, EvaluationFailure<R>>,
) -> Result<T, EvaluationFailure<R>> {
let slice = self.allowance.child_slice(MAX_MODEL_EVALUATION_WORK);
let full = slice.remaining() == MAX_MODEL_EVALUATION_WORK;
let result = admission.within_work_slice(&slice, run);
let ran_out = slice.exhausted();
let transferred = self.allowance.consume_child(&slice).is_ok();
if ran_out || !transferred {
let count = if full { &self.capped } else { &self.starved };
count.set(count.get() + 1);
}
result
}
pub(crate) fn report(&self, findings: &mut Vec<crate::report::check::Finding>) {
for (count, cap, limit) in [
(
self.capped.get(),
"MAX_MODEL_EVALUATION_WORK",
MAX_MODEL_EVALUATION_WORK,
),
(
self.starved.get(),
"MAX_MODEL_PASS_EVALUATION_WORK",
MAX_MODEL_PASS_EVALUATION_WORK,
),
] {
if count > 0 {
findings.push(crate::report::check::Finding {
check: crate::report::check::Check::GeometricConsistency,
severity: crate::report::Severity::Warning,
message: format!(
"{count} model carrier evaluations reached the local work cap {cap} \
({limit} steps); their geometric consistency checks were skipped"
),
entity: None,
related: Vec::new(),
});
}
}
}
}
impl crate::ids::comparison::sealed::TextWork for EvaluationAdmission<'_, '_> {}
impl crate::ids::comparison::TextWork for EvaluationAdmission<'_, '_> {
type Error = EvaluationRefusal;
fn comparison_work(
&self,
count: u64,
_operation: &'static str,
) -> Result<(), EvaluationRefusal> {
usize::try_from(count).map_or(Err(EvaluationRefusal::WorkExhausted), |count| {
self.work(count)
})
}
}
impl crate::index::sealed::IndexQuery for EvaluationAdmission<'_, '_> {}
impl crate::index::IndexQuery for EvaluationAdmission<'_, '_> {
type Error = EvaluationRefusal;
type Output<T> = Result<T, EvaluationRefusal>;
fn finish<T>(&self, result: Result<T, EvaluationRefusal>) -> Self::Output<T> {
result
}
fn work(&self, count: usize, _operation: &'static str) -> Result<(), EvaluationRefusal> {
EvaluationAdmission::work(*self, count)
}
}
pub(super) enum EvaluationDepthGuard<'scratch> {
Decode { _guard: DepthGuard<'scratch> },
Standard { depth: &'scratch Cell<usize> },
}
impl Drop for EvaluationDepthGuard<'_> {
fn drop(&mut self) {
if let Self::Standard { depth } = self {
depth.set(depth.get() - 1);
}
}
}
#[cfg(test)]
mod tests {
use super::EvaluationAdmission;
use crate::eval::EvaluationFailure;
use cadmpeg_core::decode::{
DecodeArena, DecodeContext, DecodePolicy, ResourceDimension, WorkBudget,
};
use cadmpeg_core::CodecError;
#[test]
fn model_text_comparison_exhausts_only_its_local_slice() {
for allowance in 0..=6_usize {
let arena = DecodeArena::new();
let policy = DecodePolicy::service();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let parent = WorkBudget::new(allowance);
let result: Result<bool, EvaluationFailure<()>> = EvaluationAdmission::Decode(&ctx)
.within_work_slice(&parent, |admission| {
crate::ids::comparison::equal(
&admission,
"alpha",
"alpha",
"model text comparison",
)
.map_err(EvaluationFailure::from)
});
assert_eq!(parent.consumed(), allowance);
if allowance < 6 {
assert_eq!(result, Err(EvaluationFailure::WorkExhausted));
assert!(parent.exhausted());
assert_eq!(ctx.resource_refusal(), None);
} else {
assert_eq!(result, Ok(true));
}
assert!(ctx.finish_session().is_ok());
}
assert_eq!(
crate::ids::comparison::compare(
&EvaluationAdmission::Standard,
"é",
"ê",
"standard text comparison"
)
.unwrap(),
std::cmp::Ordering::Less
);
}
#[test]
fn model_entries_return_the_session_depth_refusal() {
use crate::geometry::{
Curve, CurveGeometry, SolvedCurveGeometry, SolvedSurfaceGeometry, Surface,
SurfaceGeometry,
};
use crate::math::{Point3, Vector3};
let mut ir = crate::CadIr::empty();
let curve = crate::ids::CurveId::mint("test:model:curve#line").unwrap();
let surface = crate::ids::SurfaceId::mint("test:model:surface#plane").unwrap();
ir.model.curves.push(Curve {
parameter_range: None,
id: curve.clone(),
geometry: CurveGeometry::Solved(SolvedCurveGeometry::Line(
crate::geometry::analytic::LineCurve::try_new(
Point3::new(0.0, 0.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
)
.unwrap(),
)),
source_object: None,
});
ir.model.surfaces.push(Surface {
id: surface.clone(),
geometry: SurfaceGeometry::Solved(SolvedSurfaceGeometry::Plane(
crate::geometry::analytic::PlaneSurface::try_new(
Point3::new(0.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
)
.unwrap(),
)),
source_object: None,
});
let index = crate::index::ModelIndex::build(&ir);
for route in 0..4 {
let mut policy = DecodePolicy::service();
policy.limits.max_recursion_depth = 0;
let arena = DecodeArena::new();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let evaluate = || match route {
0 => crate::eval::model_curve_point_by_id(
EvaluationAdmission::Decode(&ctx),
&index,
&curve,
0.5,
),
1 => crate::eval::model_surface_point_by_id(
EvaluationAdmission::Decode(&ctx),
&index,
&surface,
0.5,
0.5,
),
2 => crate::eval::model_surface_partials_by_id(
EvaluationAdmission::Decode(&ctx),
&index,
&surface,
0.5,
0.5,
)
.map(|partials| partials.point),
_ => crate::eval::model_surface_point(
EvaluationAdmission::Decode(&ctx),
&ir,
&ir.model.surfaces[0].geometry,
0.5,
0.5,
),
};
let EvaluationFailure::ResourceLimit(first) = evaluate().unwrap_err() else {
panic!("a model depth refusal stays a resource error");
};
assert_eq!(first.dimension, ResourceDimension::RecursionDepth);
assert_eq!((first.limit, first.used), (0, 0));
assert!(first.additional > 0);
assert_eq!(evaluate(), Err(EvaluationFailure::ResourceLimit(first)));
assert!(
matches!(ctx.finish_session(), Err(CodecError::ResourceLimit(sticky)) if sticky == first)
);
}
}
#[test]
fn model_work_slice_keeps_cycles_distinct_from_resource_refusal() {
use crate::geometry::{
Curve, CurveGeometry, ProceduralCurve, ProceduralCurveDefinition, SolvedCurveGeometry,
};
let mut ir = crate::CadIr::empty();
let id = crate::ids::CurveId::mint("test:model:curve#cycle").unwrap();
ir.model.curves.push(Curve {
parameter_range: None,
id: id.clone(),
geometry: CurveGeometry::Solved(SolvedCurveGeometry::Unknown { record: None }),
source_object: None,
});
ir.model
.add_procedural_curve(
&id,
ProceduralCurve::new(
crate::ids::ProceduralCurveId::mint("test:model:procedural#cycle").unwrap(),
ProceduralCurveDefinition::Replica {
source: id.clone(),
transform: crate::transform::Transform::identity(),
},
),
)
.unwrap();
let index = crate::index::ModelIndex::build(&ir);
let arena = DecodeArena::new();
let policy = DecodePolicy::service();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let parent = WorkBudget::new(100_000);
assert_eq!(
EvaluationAdmission::Decode(&ctx).within_work_slice(&parent, |admission| {
crate::eval::model_curve_point_by_id(admission, &index, &id, 0.5)
}),
Err(EvaluationFailure::NoValue)
);
assert!(ctx.finish_session().is_ok());
}
fn doubling_model(levels: usize) -> (crate::CadIr, crate::ids::CurveId) {
use crate::geometry::pcurve::{LinePcurve, PcurveGeometry};
use crate::geometry::surface_payloads::AxisRevolutionSurfaceConstruction;
use crate::geometry::{
Curve, CurveGeometry, ProceduralCurve, ProceduralCurveDefinition, ProceduralSurface,
ProceduralSurfaceDefinition, SolvedCurveGeometry, SolvedSurfaceGeometry, Surface,
SurfaceGeometry, TolerantIntersectionConstruction,
TolerantIntersectionParameterization,
};
use crate::ids::{CurveId, ProceduralCurveId, ProceduralSurfaceId, SurfaceId};
use crate::math::{Point2, Point3, Vector3};
let line = |id: &CurveId| Curve {
parameter_range: None,
id: id.clone(),
geometry: CurveGeometry::Solved(SolvedCurveGeometry::Line(
crate::geometry::analytic::LineCurve::try_new(
Point3::new(0.0, 0.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
)
.unwrap(),
)),
source_object: None,
};
let pcurve = PcurveGeometry::Line(
LinePcurve::try_new(Point2::new(0.0, 0.0), Point2::new(1.0, 0.0)).unwrap(),
);
let mut ir = crate::CadIr::empty();
let mut below = CurveId::mint("test:model:curve#0").unwrap();
ir.model.curves.push(line(&below));
for level in 1..=levels {
let supports = [0, 1]
.map(|side| SurfaceId::mint(format!("test:model:surface#{level}-{side}")).unwrap());
for (side, support) in supports.iter().enumerate() {
ir.model.surfaces.push(Surface {
id: support.clone(),
geometry: SurfaceGeometry::Solved(SolvedSurfaceGeometry::Plane(
crate::geometry::analytic::PlaneSurface::try_new(
Point3::new(0.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
)
.unwrap(),
)),
source_object: None,
});
ir.model
.add_procedural_surface(
support,
ProceduralSurface::new(
ProceduralSurfaceId::mint(format!(
"test:model:procedural#surface-{level}-{side}"
))
.unwrap(),
ProceduralSurfaceDefinition::AxisRevolution(
AxisRevolutionSurfaceConstruction::try_new(
below.clone(),
Point3::new(0.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
)
.unwrap(),
),
None,
),
)
.unwrap();
}
let curve = CurveId::mint(format!("test:model:curve#{level}")).unwrap();
ir.model.curves.push(line(&curve));
ir.model
.add_procedural_curve(
&curve,
ProceduralCurve::new(
ProceduralCurveId::mint(format!("test:model:procedural#curve-{level}"))
.unwrap(),
ProceduralCurveDefinition::TolerantIntersection {
construction: TolerantIntersectionConstruction::try_new(
supports,
[Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.0)],
1.0e-9,
)
.unwrap(),
parameterization: Some(
TolerantIntersectionParameterization::try_new(
[pcurve.clone(), pcurve.clone()],
[-1.0, 1.0],
)
.unwrap(),
),
cache: None,
},
),
)
.unwrap();
below = curve;
}
(ir, below)
}
#[test]
fn decode_model_evaluation_has_a_local_work_cap() {
let evaluate = |levels| {
let (ir, curve) = doubling_model(levels);
let index = crate::index::ModelIndex::build(&ir);
let arena = DecodeArena::new();
let policy = DecodePolicy::service();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let result = crate::eval::model_curve_point_by_id(
EvaluationAdmission::Decode(&ctx),
&index,
&curve,
0.5,
);
assert_eq!(ctx.resource_refusal(), None);
assert!(ctx.finish_session().is_ok());
result
};
assert!(evaluate(2).is_ok());
assert_eq!(evaluate(18), Err(EvaluationFailure::WorkExhausted));
}
#[test]
fn standard_model_evaluation_has_a_local_work_cap() {
let evaluate = |levels| {
let (ir, curve) = doubling_model(levels);
let index = crate::index::ModelIndex::build(&ir);
crate::eval::model_curve_point_by_id(EvaluationAdmission::Standard, &index, &curve, 0.5)
};
assert!(evaluate(2).is_ok());
assert_eq!(evaluate(18), Err(EvaluationFailure::WorkExhausted));
}
#[test]
fn validation_pass_evaluations_share_one_allowance() {
const EVALUATIONS: usize = 1 << 12;
let (ir, curve) = doubling_model(18);
let index = crate::index::ModelIndex::build(&ir);
let arena = DecodeArena::new();
let policy = DecodePolicy::service();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let evaluate = |pass: &super::ModelEvaluationPass, index, curve| {
pass.evaluate(EvaluationAdmission::Decode(&ctx), |admission| {
crate::eval::model_curve_point_by_id(admission, index, curve, 0.5)
})
};
let pass = super::ModelEvaluationPass::with_allowance(super::MAX_MODEL_EVALUATION_WORK + 1);
for _ in 0..EVALUATIONS {
assert_eq!(
evaluate(&pass, &index, &curve),
Err(EvaluationFailure::WorkExhausted)
);
}
assert_eq!(pass.allowance.remaining(), 0);
let (cheap_ir, cheap_curve) = doubling_model(2);
let cheap_index = crate::index::ModelIndex::build(&cheap_ir);
assert_eq!(
evaluate(&pass, &cheap_index, &cheap_curve),
Err(EvaluationFailure::WorkExhausted)
);
let mut findings = Vec::new();
pass.report(&mut findings);
let messages: Vec<_> = findings
.iter()
.map(|finding| (finding.severity, finding.message.as_str()))
.collect();
assert_eq!(
messages,
[
(
crate::report::Severity::Warning,
"1 model carrier evaluations reached the local work cap \
MAX_MODEL_EVALUATION_WORK (16777216 steps); their geometric consistency \
checks were skipped"
),
(
crate::report::Severity::Warning,
"4096 model carrier evaluations reached the local work cap \
MAX_MODEL_PASS_EVALUATION_WORK (1073741824 steps); their geometric \
consistency checks were skipped"
),
]
);
let fresh = super::ModelEvaluationPass::new();
assert!(evaluate(&fresh, &cheap_index, &cheap_curve).is_ok());
let mut findings = Vec::new();
fresh.report(&mut findings);
assert!(findings.is_empty());
assert_eq!(ctx.resource_refusal(), None);
assert!(ctx.finish_session().is_ok());
}
#[test]
fn validation_reports_a_capped_endpoint_evaluation_as_skipped() {
let (ir, _) = doubling_model(16);
let report = crate::validate::validate_neutral(&ir, Vec::new()).unwrap();
assert!(report.findings.iter().any(|finding| {
finding.severity == crate::report::Severity::Warning
&& finding.message.contains("MAX_MODEL_EVALUATION_WORK")
}));
assert!(!report.findings.iter().any(|finding| {
finding.entity.as_deref() == Some("test:model:procedural#curve-16")
&& finding.message.contains("does not evaluate")
}));
}
#[test]
fn exhausted_work_slice_leaves_the_session_unrefused() {
let arena = DecodeArena::new();
let policy = DecodePolicy::service();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let parent = WorkBudget::new(0);
let result: Result<(), EvaluationFailure<()>> = EvaluationAdmission::Decode(&ctx)
.within_work_slice(&parent, |admission| {
admission.work(1).map_err(EvaluationFailure::from)
});
assert_eq!(result, Err(EvaluationFailure::WorkExhausted));
assert!(parent.exhausted());
assert_eq!(ctx.resource_refusal(), None);
let called = std::cell::Cell::new(false);
let repeated: Result<(), EvaluationFailure<()>> = EvaluationAdmission::Decode(&ctx)
.within_work_slice(&parent, |admission| {
called.set(true);
admission.work(1).map_err(EvaluationFailure::from)
});
assert!(called.get());
assert_eq!(repeated, Err(EvaluationFailure::WorkExhausted));
assert!(ctx.finish_session().is_ok());
}
#[test]
fn work_slice_preserves_active_session_depth() {
let mut policy = DecodePolicy::service();
policy.limits.max_recursion_depth = 1;
let arena = DecodeArena::new();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let outer = ctx.enter_nested_limit("test outer evaluation").unwrap();
let parent = WorkBudget::new(10);
let depth = std::cell::Cell::new(0);
let result: Result<(), EvaluationFailure<()>> = EvaluationAdmission::Decode(&ctx)
.within_work_slice(&parent, |admission| {
admission
.enter(&depth)
.map(|_guard| ())
.map_err(EvaluationFailure::ResourceLimit)
});
let EvaluationFailure::ResourceLimit(first) = result.unwrap_err() else {
panic!("a child slice shares active depth");
};
assert_eq!(first.dimension, ResourceDimension::RecursionDepth);
assert_eq!((first.limit, first.used, first.additional), (1, 1, 1));
drop(outer);
assert!(
matches!(ctx.finish_session(), Err(CodecError::ResourceLimit(sticky)) if sticky == first)
);
}
}