use super::*; use crate::analysis_numeric::{ Dimension, Exact, ExactWire, NumericError, NumericType, NumericValue, ResolvedKind, WorkLimits, WorkMeter, }; use crate::*; use std::collections::BTreeMap; fn id(value: &str) -> Id { Id::parse_str(value).unwrap() } fn reference(value: Id) -> DefinitionRef { DefinitionRef::Local(value) } fn number(value: i64) -> Expr { Expr::Number(Exact::from_integer(value)) } fn expression(value: Expr) -> Binding { Binding::Expression(value) } fn quantity(value: i64, unit: Id) -> Expr { Expr::Quantity { magnitude: Exact::from_integer(value), unit: reference(unit), } } fn scalar_number() -> AnalysisType { AnalysisType::Scalar(ScalarType::Number) } fn duration_type(kind: Id) -> AnalysisType { AnalysisType::Scalar(ScalarType::Quantity(reference(kind))) } fn binary(op: BinaryOp, left: Expr, right: Expr) -> Expr { Expr::Binary { op, left: Box::new(left), right: Box::new(right), } } fn exact(numerator: &str, denominator: &str) -> Exact { Exact::from_wire( &ExactWire { numerator: numerator.into(), denominator: denominator.into(), }, &mut WorkMeter::new(WorkLimits::REFERENCE), ) .unwrap() } fn definition(id: Id, alias: &str, body: DefinitionBody) -> Definition { Definition { id, alias: alias.into(), name: alias.into(), description: String::new(), body, } } fn shared(id: Id, alias: &str, body: SharedBody) -> Element { Element { id, alias: alias.into(), name: alias.into(), description: String::new(), parent: None, body: ElementBody::shared_in(Layer::Oa, body), } } fn activity(id: Id, alias: &str, parent: Option) -> Element { Element { id, alias: alias.into(), name: alias.into(), description: String::new(), parent, body: ElementBody::Oa(OaBody::Activity), } } /// The independently specified complete 17-UUID response-budget fixture. /// This uses the real Project; no alternate model, source parser, or evaluator. struct Fixture { project: Project, duration: Id, second: Id, millisecond: Id, combine: Id, a: ParameterId, b: ParameterId, respond: Id, receive: Id, dispatch: Id, receive_time: Id, dispatch_time: Id, total: Id, limit: Id, check: Id, degraded: Id, unmeasured: Id, } fn fixture() -> Fixture { let project_id = id("4210b9f2-7046-47f0-aa3b-153f626a579d"); let duration = id("188b911c-5f8b-431c-9eea-2d41f3953767"); let second = id("534f4672-1ba5-4157-8b0a-b9cf41d0a3ce"); let millisecond = id("39053f37-fec9-4708-9f0a-3959d223f891"); let combine = id("fd7ce7ab-f789-40bc-b699-0c37f1b6902f"); let a = ParameterId(id("28ff40b0-1b8f-4735-8536-5a6ab21b56cb")); let b = ParameterId(id("7a47cc1e-8ae5-4551-b2d8-8a68108e0976")); let respond = id("cabec6a1-7d5d-48f3-8d2d-a7fc8cf78f59"); let receive = id("4ab9a8a7-7552-488d-9862-e14b89e7b353"); let dispatch = id("6df991ba-4c5e-4535-99e2-524ae274ee8b"); let receive_time = id("63a931c2-675a-4fce-89e5-04361cb99450"); let dispatch_time = id("c33fc772-2a60-4044-9af9-73c61c563ace"); let total = id("bd5b68e8-65fc-4e35-9071-a73061316a4b"); let limit = id("437bedbb-2cf4-44ee-a139-6563cc5086ec"); let check = id("bac0de99-9d2f-4e53-9158-d424ecac61bf"); let degraded = id("38036c65-16d4-449c-837b-91eb141c1d93"); let unmeasured = id("99d3e09a-0ec3-4194-9dd4-9f3de6625cde"); let definitions = [ definition( duration, "duration", DefinitionBody::QuantityKind(QuantityKindDefinition { dimension: Dimension::new([0, 0, 1, 0, 0, 0, 0]).unwrap(), algebra: QuantityAlgebra::Linear, }), ), definition( second, "second", DefinitionBody::Unit(UnitDefinition { kind: reference(duration), scale: Exact::from_integer(1), offset: Exact::from_integer(0), symbol: "s".into(), }), ), definition( millisecond, "millisecond", DefinitionBody::Unit(UnitDefinition { kind: reference(duration), scale: exact("1", "1000"), offset: Exact::from_integer(0), symbol: "ms".into(), }), ), definition( combine, "combine", DefinitionBody::Calculation(CalculationDefinition { parameters: vec![ Parameter { id: a, name: "a".into(), value_type: duration_type(duration), }, Parameter { id: b, name: "b".into(), value_type: duration_type(duration), }, ], result: duration_type(duration), body: expression(binary( BinaryOp::Add, Expr::Parameter(a), Expr::Parameter(b), )), }), ), ] .into_iter() .map(|definition| (definition.id, definition)) .collect(); let property = |id, alias: &str, subject, display, base| { shared( id, alias, SharedBody::EngineeringProperty { subject: AnalysisSubject::Element(subject), value_type: duration_type(duration), display_unit: Some(reference(display)), base: expression(base), }, ) }; let case = |id, alias: &str, binding| { shared( id, alias, SharedBody::AnalysisCase { subject: None, base: BaselineBase::ModelBaseline, overrides: BTreeMap::from([(receive_time, binding)]), }, ) }; let elements = [ activity(respond, "respond", None), activity(receive, "receive", Some(respond)), activity(dispatch, "dispatch", Some(respond)), property( receive_time, "receive_time", receive, millisecond, quantity(750, millisecond), ), property( dispatch_time, "dispatch_time", dispatch, second, quantity(1, second), ), property( total, "total_time", respond, second, Expr::Call { definition: reference(combine), arguments: BTreeMap::from([ (a, Expr::Property(receive_time)), (b, Expr::Property(dispatch_time)), ]), }, ), property(limit, "limit", respond, second, quantity(2, second)), shared( check, "budget", SharedBody::AnalysisCheck { subject: None, documented_constraint: None, predicate: expression(binary( BinaryOp::LessOrEqual, Expr::Property(total), Expr::Property(limit), )), }, ), case( degraded, "degraded", expression(quantity(1250, millisecond)), ), case(unmeasured, "unmeasured", Binding::Unbound), ] .into_iter() .map(|element| (element.id, element)) .collect(); let project = Project { schema_version: 8, id: project_id, alias: "response_budget".into(), name: "Response budget".into(), description: String::new(), definitions, elements, relations: BTreeMap::new(), diagrams: BTreeMap::new(), artifact_links: BTreeMap::new(), traces: BTreeMap::new(), }; Fixture { project, duration, second, millisecond, combine, a, b, respond, receive, dispatch, receive_time, dispatch_time, total, limit, check, degraded, unmeasured, } } fn reject(project: &Project) -> AnalysisError { match admit(project, AdmissionLimits::REFERENCE) { Ok(_) => panic!("Expected analysis rejection"), Err(error) => error, } } fn base_mut(project: &mut Project, id: Id) -> &mut Binding { match project.elements.get_mut(&id).unwrap().shared_mut().unwrap() { SharedBody::EngineeringProperty { base, .. } => base, _ => panic!(), } } fn calculation_mut(project: &mut Project, id: Id) -> &mut CalculationDefinition { match &mut project.definitions.get_mut(&id).unwrap().body { DefinitionBody::Calculation(value) => value, _ => panic!(), } } fn overrides_mut(project: &mut Project, id: Id) -> &mut BTreeMap { match project.elements.get_mut(&id).unwrap().shared_mut().unwrap() { SharedBody::AnalysisCase { overrides, .. } => overrides, _ => panic!(), } } fn add_calculation(project: &mut Project, n: u128, result: AnalysisType, body: Expr) -> Id { let id = Id::from_u128(n); project.definitions.insert( id, definition( id, &format!("calc_{n}"), DefinitionBody::Calculation(CalculationDefinition { parameters: vec![], result, body: expression(body), }), ), ); id } fn add_identity_calculation( project: &mut Project, n: u128, value_type: AnalysisType, ) -> (Id, ParameterId) { let id = Id::from_u128(n); let parameter = ParameterId(Id::from_u128(n + 10_000)); project.definitions.insert( id, definition( id, &format!("identity_{n}"), DefinitionBody::Calculation(CalculationDefinition { parameters: vec![Parameter { id: parameter, name: "value".into(), value_type: value_type.clone(), }], result: value_type, body: expression(Expr::Parameter(parameter)), }), ), ); (id, parameter) } #[test] fn complete_real_model_roundtrips_and_exposes_explicit_effective_bindings() { let f = fixture(); assert_eq!(f.project.identities().len(), 15); // Scoped parameters add the other two identities. assert!(!f.project.contains(f.a.0)); assert!(!f.project.contains(f.b.0)); assert_eq!(f.project.layer_of(f.duration), None); assert_eq!(f.project.elements[&f.dispatch].parent, Some(f.respond)); assert!(matches!( f.project.elements[&f.limit].shared(), Some(SharedBody::EngineeringProperty { .. }) )); let environment = admit(&f.project, AdmissionLimits::REFERENCE).unwrap(); assert_eq!(environment.admission_usage().stored_expression_nodes, 13); assert_eq!( environment.unit(reference(f.second)).unwrap().scale(), &Exact::from_integer(1) ); assert_eq!( environment.unit(reference(f.millisecond)).unwrap().scale(), &exact("1", "1000") ); let baseline = environment .effective_binding(f.receive_time, EvaluationContext::ModelBaseline) .unwrap(); assert_eq!(baseline.origin, BindingOrigin::ModelBaseline); assert_eq!(baseline.binding, &expression(quantity(750, f.millisecond))); let alternative = environment .effective_binding(f.receive_time, EvaluationContext::Case(f.degraded)) .unwrap(); assert_eq!(alternative.origin, BindingOrigin::CaseOverride(f.degraded)); assert_eq!( alternative.binding, &expression(quantity(1250, f.millisecond)) ); let missing = environment .effective_binding(f.receive_time, EvaluationContext::Case(f.unmeasured)) .unwrap(); assert_eq!(missing.binding, &Binding::Unbound); assert_eq!(missing.origin, BindingOrigin::CaseOverride(f.unmeasured)); // Inherited formula remains the model base; a future evaluator must retain // Case(degraded) while resolving each Property read within that formula. let formula = environment .effective_binding(f.total, EvaluationContext::Case(f.degraded)) .unwrap(); assert_eq!(formula.origin, BindingOrigin::ModelBaseline); assert!(matches!( formula.binding, Binding::Expression(Expr::Call { .. }) )); let encoded = serde_json::to_vec(&f.project).unwrap(); let decoded = persistence::decode_with_report(&encoded).unwrap(); assert_eq!(decoded.project, f.project); assert!(decoded.migration.is_none()); } #[test] fn narrow_override_commands_preserve_missing_clear_and_history() { let f = fixture(); let mut session = Session::new(f.project.clone()); session .execute(Command::ClearAnalysisOverride { case: f.unmeasured, property: f.receive_time, }) .unwrap(); assert_eq!(session.revision(), 1); assert_eq!( admit(session.project(), AdmissionLimits::REFERENCE) .unwrap() .effective_binding(f.receive_time, EvaluationContext::Case(f.unmeasured)) .unwrap() .origin, BindingOrigin::ModelBaseline ); let cleared = session.project().clone(); session .execute(Command::ClearAnalysisOverride { case: f.unmeasured, property: f.receive_time, }) .unwrap(); assert_eq!(session.revision(), 1); // Absent-key clear is an exact no-op. session .execute(Command::SetAnalysisOverride { case: f.unmeasured, property: f.receive_time, binding: Binding::Unbound, }) .unwrap(); assert_eq!(session.revision(), 2); assert_eq!(session.project(), &f.project); session.undo(); assert_eq!(session.project(), &cleared); session.redo(); assert_eq!(session.project(), &f.project); let before = session.project().clone(); assert!( session .execute(Command::SetAnalysisBase { property: f.receive_time, binding: expression(number(1)) }) .is_err() ); assert_eq!(session.project(), &before); assert_eq!(session.revision(), 4); } #[test] fn global_reidentify_rewrites_typed_domains_but_preserves_scoped_parameters() { let f = fixture(); let fresh_property = id("9591f6b0-4ea4-4b6b-9e28-53e128e92904"); let fresh_kind = id("81b0741b-4e5b-4b1c-a6b5-cf289d1322e1"); let prepared = prepare_command( &f.project, &Command::Batch(vec![ Command::Reidentify(f.receive_time, fresh_property), Command::Reidentify(f.duration, fresh_kind), ]), ) .unwrap(); assert_eq!(prepared.identity_map[&f.receive_time], fresh_property); assert_eq!(prepared.identity_map[&f.duration], fresh_kind); assert!(!prepared.project.contains(f.duration)); assert!(!prepared.project.contains(f.receive_time)); let environment = admit(&prepared.project, AdmissionLimits::REFERENCE).unwrap(); let calculation = environment.calculation(reference(f.combine)).unwrap(); assert_eq!(calculation.parameters[0].id, f.a); assert_eq!(calculation.parameters[1].id, f.b); assert_eq!( calculation.parameters[0].value_type, duration_type(fresh_kind) ); let Some(SharedBody::AnalysisCase { overrides, .. }) = prepared.project.elements[&f.degraded].shared() else { panic!() }; assert!(overrides.contains_key(&fresh_property)); assert!(!overrides.contains_key(&f.receive_time)); let Binding::Expression(Expr::Call { arguments, .. }) = environment .effective_binding(f.total, EvaluationContext::ModelBaseline) .unwrap() .binding else { panic!() }; assert_eq!(arguments[&f.a], Expr::Property(fresh_property)); // A global identity may equal a parameter's scoped UUID without becoming it. let mut coincident = f.project.clone(); let scoped_alias = definition( f.a.0, "scoped_collision_is_local", DefinitionBody::QuantityKind(QuantityKindDefinition { dimension: Dimension::ZERO, algebra: QuantityAlgebra::Linear, }), ); coincident.definitions.insert(scoped_alias.id, scoped_alias); let moved = execute_batch( &coincident, &[Command::Reidentify(f.a.0, Id::from_u128(9042))], ) .unwrap(); assert_eq!( admit(&moved, AdmissionLimits::REFERENCE) .unwrap() .calculation(reference(f.combine)) .unwrap() .parameters[0] .id, f.a ); } #[test] fn map_key_collision_is_rejected_before_any_reference_mutation() { let mut f = fixture(); overrides_mut(&mut f.project, f.degraded) .insert(f.dispatch_time, expression(quantity(1, f.second))); let before = f.project.clone(); assert!( crate::references::remap_references(&mut f.project, f.receive_time, f.dispatch_time) .is_err() ); assert_eq!(f.project, before); } #[test] fn deletion_requires_explicit_repairs_in_the_same_atomic_batch() { let f = fixture(); assert!(execute_batch(&f.project, &[Command::Delete(f.millisecond)]).is_err()); assert!(execute_batch(&f.project, &[Command::Delete(f.receive_time)]).is_err()); assert!(execute_batch(&f.project, &[Command::Delete(f.receive)]).is_err()); let fixed = execute_batch( &f.project, &[ Command::SetAnalysisBase { property: f.total, binding: expression(Expr::Property(f.dispatch_time)), }, Command::ClearAnalysisOverride { case: f.degraded, property: f.receive_time, }, Command::ClearAnalysisOverride { case: f.unmeasured, property: f.receive_time, }, Command::Delete(f.receive_time), ], ) .unwrap(); assert!(!fixed.contains(f.receive_time)); assert!(fixed.contains(f.receive)); admit(&fixed, AdmissionLimits::REFERENCE).unwrap(); } #[test] fn every_case_is_checked_and_baseline_edits_cannot_introduce_a_case_only_cycle() { let mut f = fixture(); overrides_mut(&mut f.project, f.degraded).remove(&f.receive_time); overrides_mut(&mut f.project, f.degraded) .insert(f.dispatch_time, expression(Expr::Property(f.receive_time))); admit(&f.project, AdmissionLimits::REFERENCE).unwrap(); let mut session = Session::new(f.project.clone()); assert!( session .execute(Command::SetAnalysisBase { property: f.receive_time, binding: expression(Expr::Property(f.dispatch_time)) }) .is_err() ); assert_eq!(session.project(), &f.project); assert_eq!(session.revision(), 0); overrides_mut(&mut f.project, f.degraded) .insert(f.receive_time, expression(Expr::Property(f.total))); assert_eq!(reject(&f.project).code, "analysis_cycle"); } #[test] fn unused_branches_still_reject_wrong_types_references_and_cycles() { let f = fixture(); for unused in [ number(0), Expr::Property(f.respond), Expr::Property(f.receive_time), ] { let mut candidate = f.project.clone(); *base_mut(&mut candidate, f.receive_time) = expression(Expr::If { condition: Box::new(Expr::Boolean(true)), when_true: Box::new(quantity(750, f.millisecond)), when_false: Box::new(unused), }); assert!(admit(&candidate, AdmissionLimits::REFERENCE).is_err()); } } #[test] fn calculations_are_pure_parameters_are_scoped_and_calls_bind_exactly() { let f = fixture(); let mut captured = f.project.clone(); calculation_mut(&mut captured, f.combine).body = expression(Expr::Property(f.receive_time)); assert_eq!(reject(&captured).code, "analysis_purity"); let mut duplicate = f.project.clone(); calculation_mut(&mut duplicate, f.combine).parameters[1].id = f.a; assert_eq!(reject(&duplicate).code, "analysis_parameter"); let mut absent = f.project.clone(); *base_mut(&mut absent, f.total) = expression(Expr::Call { definition: reference(f.combine), arguments: BTreeMap::from([(f.a, Expr::Property(f.receive_time))]), }); assert_eq!(reject(&absent).code, "analysis_arguments"); let mut escaped = f.project.clone(); *base_mut(&mut escaped, f.total) = expression(Expr::Parameter(f.a)); assert_eq!(reject(&escaped).code, "analysis_parameter"); let mut renamed = f.project.clone(); let mut revised = renamed.definitions[&f.combine].clone(); let DefinitionBody::Calculation(calculation) = &mut revised.body else { panic!() }; calculation.parameters[0].name = "first_duration".into(); renamed = execute_batch(&renamed, &[Command::EditDefinition(f.combine, revised)]).unwrap(); admit(&renamed, AdmissionLimits::REFERENCE).unwrap(); // Distinct errors make argument order observable. Parameter a sorts before // b by UUID; reversing declaration order must reverse the first error. let mut ordered = f.project.clone(); *base_mut(&mut ordered, f.total) = expression(Expr::Call { definition: reference(f.combine), arguments: BTreeMap::from([ (f.a, Expr::Parameter(f.a)), (f.b, Expr::Property(f.respond)), ]), }); assert_eq!(reject(&ordered).code, "analysis_parameter"); calculation_mut(&mut ordered, f.combine) .parameters .swap(0, 1); assert_eq!(reject(&ordered).code, "analysis_property"); *base_mut(&mut ordered, f.total) = expression(binary( BinaryOp::Add, Expr::Property(f.respond), Expr::Parameter(f.a), )); assert_eq!(reject(&ordered).code, "analysis_property"); *base_mut(&mut ordered, f.total) = expression(binary( BinaryOp::Add, Expr::Parameter(f.a), Expr::Property(f.respond), )); assert_eq!(reject(&ordered).code, "analysis_parameter"); } #[test] fn layers_subject_kinds_and_display_units_are_strict() { let f = fixture(); let mut different_layer = f.project.clone(); let element = different_layer.elements.get_mut(&f.degraded).unwrap(); element.body = ElementBody::shared_in(Layer::Sa, element.shared().unwrap().clone()); assert_eq!(reject(&different_layer).code, "analysis_layer"); let mut wrong_subject = f.project.clone(); let Some(SharedBody::EngineeringProperty { subject, .. }) = wrong_subject .elements .get_mut(&f.receive_time) .unwrap() .shared_mut() else { panic!() }; *subject = AnalysisSubject::Element(f.duration); assert_eq!(reject(&wrong_subject).code, "analysis_subject"); let mut wrong_display = f.project.clone(); let Some(SharedBody::EngineeringProperty { display_unit, .. }) = wrong_display .elements .get_mut(&f.receive_time) .unwrap() .shared_mut() else { panic!() }; *display_unit = Some(reference(f.duration)); assert_eq!(reject(&wrong_display).code, "analysis_unit"); assert!(ElementBody::new(Kind::EngineeringProperty, Layer::Oa).is_none()); assert!( admit(&f.project, AdmissionLimits::REFERENCE) .unwrap() .effective_binding(f.total, EvaluationContext::Case(f.check)) .is_err() ); } #[test] fn equal_dimensions_do_not_create_nominal_compatibility() { let mut f = fixture(); let other_kind = Id::from_u128(4001); let other_unit = Id::from_u128(4002); f.project.definitions.insert( other_kind, definition( other_kind, "other_duration", DefinitionBody::QuantityKind(QuantityKindDefinition { dimension: Dimension::new([0, 0, 1, 0, 0, 0, 0]).unwrap(), algebra: QuantityAlgebra::Linear, }), ), ); f.project.definitions.insert( other_unit, definition( other_unit, "other_second", DefinitionBody::Unit(UnitDefinition { kind: reference(other_kind), scale: Exact::from_integer(1), offset: Exact::from_integer(0), symbol: "s".into(), }), ), ); *base_mut(&mut f.project, f.receive_time) = expression(quantity(1, other_unit)); assert_eq!(reject(&f.project).code, "analysis_type"); } #[test] fn static_types_do_not_evaluate_domain_conditions_or_check_truth() { let mut f = fixture(); add_calculation( &mut f.project, 5001, scalar_number(), binary(BinaryOp::Divide, number(0), number(0)), ); let Some(SharedBody::AnalysisCheck { predicate, .. }) = f.project.elements.get_mut(&f.check).unwrap().shared_mut() else { panic!() }; *predicate = expression(Expr::Boolean(false)); admit(&f.project, AdmissionLimits::REFERENCE).unwrap(); let zero = NumericValue::::number(Exact::from_integer(0)); assert_eq!( NumericType::::Number .multiplicative_result(&NumericType::Number, true) .unwrap(), NumericType::Number ); assert_eq!( zero.divide(&zero, &mut WorkMeter::new(WorkLimits::REFERENCE)), Err(NumericError::DivisionByZero) ); assert_eq!( NumericType::::Number.power_result(0).unwrap(), NumericType::Number ); assert_eq!( zero.power(0, &mut WorkMeter::new(WorkLimits::REFERENCE)), Err(NumericError::ZeroPowerZero) ); } #[test] fn anonymous_dimension_algebra_and_affine_rules_share_pure_runtime_helpers() { let time = Dimension::new([0, 0, 1, 0, 0, 0, 0]).unwrap(); let interval = ResolvedKind::linear(1u8, time); let point = ResolvedKind::affine(2u8, &interval).unwrap(); let named = NumericType::Named(interval.clone()); let affine = NumericType::Named(point.clone()); let anonymous = NumericType::Anonymous(time); assert_eq!( named.multiplicative_result(&named, true).unwrap(), NumericType::Number ); assert_eq!( anonymous .multiplicative_result(&NumericType::Number, false) .unwrap(), anonymous ); assert!(named.additive_result(&anonymous, false).is_err()); assert_eq!(affine.additive_result(&affine, true).unwrap(), named); assert_eq!(affine.additive_result(&named, false).unwrap(), affine); assert!(affine.additive_result(&affine, false).is_err()); assert!( affine .multiplicative_result(&NumericType::Number, false) .is_err() ); assert!(named.additive_result(&affine, true).is_err()); assert!(affine.unary_result().is_err()); assert!(named.assertion_result(&interval).is_err()); assert_eq!(anonymous.assertion_result(&interval).unwrap(), named); for (left, right, subtract, expected) in [ (affine.clone(), affine.clone(), true, named.clone()), (affine.clone(), named.clone(), false, affine.clone()), (named.clone(), affine.clone(), false, affine.clone()), (named.clone(), named.clone(), false, named.clone()), ] { assert_eq!(left.additive_result(&right, subtract).unwrap(), expected); let left = NumericValue::new(left, Exact::from_integer(8)); let right = NumericValue::new(right, Exact::from_integer(3)); let mut meter = WorkMeter::new(WorkLimits::REFERENCE); let result = if subtract { left.subtract(&right, &mut meter) } else { left.add(&right, &mut meter) } .unwrap(); assert_eq!(result.value_type(), &expected); assert_eq!( result.coordinate(), &Exact::from_integer(if subtract { 5 } else { 11 }) ); } } #[test] fn affine_definitions_require_linear_matching_difference_and_valid_units() { let mut f = fixture(); let point = Id::from_u128(5200); f.project.definitions.insert( point, definition( point, "time_point", DefinitionBody::QuantityKind(QuantityKindDefinition { dimension: Dimension::ZERO, algebra: QuantityAlgebra::Affine { difference: reference(f.duration), }, }), ), ); assert_eq!(reject(&f.project).code, "analysis_kind"); let DefinitionBody::QuantityKind(kind) = &mut f.project.definitions.get_mut(&point).unwrap().body else { panic!() }; kind.dimension = Dimension::new([0, 0, 1, 0, 0, 0, 0]).unwrap(); admit(&f.project, AdmissionLimits::REFERENCE).unwrap(); let DefinitionBody::Unit(unit) = &mut f.project.definitions.get_mut(&f.second).unwrap().body else { panic!() }; unit.offset = Exact::from_integer(1); assert_eq!(reject(&f.project).code, "analysis_unit"); } #[test] fn empty_lists_require_context_and_lists_are_flat_bounded_homogeneous() { let mut f = fixture(); let good = add_calculation( &mut f.project, 6000, AnalysisType::List(ScalarType::Number), Expr::List(vec![]), ); add_calculation( &mut f.project, 6001, scalar_number(), Expr::Sum { values: Box::new(Expr::List(vec![])), initial: Box::new(number(0)), }, ); admit(&f.project, AdmissionLimits::REFERENCE).unwrap(); for items in [ vec![number(0), Expr::Boolean(true)], vec![Expr::List(vec![])], vec![number(0); 1025], ] { calculation_mut(&mut f.project, good).body = expression(Expr::List(items)); assert!(admit(&f.project, AdmissionLimits::REFERENCE).is_err()); } calculation_mut(&mut f.project, good).body = expression(Expr::List(vec![number(0); 1024])); admit(&f.project, AdmissionLimits::REFERENCE).unwrap(); } #[test] fn logical_depth_and_call_depth_have_exact_boundaries() { let mut f = fixture(); let mut nested = number(1); for _ in 1..128 { nested = Expr::Unary { op: UnaryOp::Negate, operand: Box::new(nested), }; } let deep = add_calculation(&mut f.project, 7000, scalar_number(), nested); eprintln!("analysis depth gate: admitting 128 unary nodes"); admit(&f.project, AdmissionLimits::REFERENCE).unwrap(); let old = std::mem::replace( &mut calculation_mut(&mut f.project, deep).body, Binding::Unbound, ); let Binding::Expression(old) = old else { panic!() }; calculation_mut(&mut f.project, deep).body = expression(Expr::Unary { op: UnaryOp::Negate, operand: Box::new(old), }); eprintln!("analysis depth gate: rejecting 129 unary nodes"); assert_eq!(reject(&f.project).code, "analysis_depth"); // Only 129 nodes here: no hostile unbounded recursive teardown probe. f.project.definitions.remove(&deep); let (identity, parameter) = add_identity_calculation(&mut f.project, 7001, scalar_number()); let mut mixed = number(1); for level in 1..128 { mixed = match level % 5 { 0 => Expr::Unary { op: UnaryOp::Absolute, operand: Box::new(mixed), }, 1 => binary(BinaryOp::Add, mixed, number(0)), 2 => Expr::If { condition: Box::new(Expr::Boolean(true)), when_true: Box::new(mixed), when_false: Box::new(number(0)), }, 3 => Expr::Call { definition: reference(identity), arguments: BTreeMap::from([(parameter, mixed)]), }, _ => Expr::Power { base: Box::new(mixed), exponent: 1, }, }; } let mixed_id = add_calculation(&mut f.project, 7002, scalar_number(), mixed); eprintln!("analysis depth gate: admitting mixed Binary/If/Call/Power/Unary depth 128"); admit(&f.project, AdmissionLimits::REFERENCE).unwrap(); let Binding::Expression(mixed) = std::mem::replace( &mut calculation_mut(&mut f.project, mixed_id).body, Binding::Unbound, ) else { panic!() }; calculation_mut(&mut f.project, mixed_id).body = expression(Expr::Unary { op: UnaryOp::Negate, operand: Box::new(mixed), }); eprintln!("analysis depth gate: rejecting mixed depth 129"); assert_eq!(reject(&f.project).code, "analysis_depth"); f.project.definitions.remove(&mixed_id); let list_type = AnalysisType::List(ScalarType::Number); let (list_identity, list_parameter) = add_identity_calculation(&mut f.project, 7003, list_type.clone()); let mut list = Expr::List(vec![]); for level in 1..128 { list = if level % 2 == 0 { Expr::If { condition: Box::new(Expr::Boolean(false)), when_true: Box::new(list), when_false: Box::new(Expr::List(vec![])), } } else { Expr::Call { definition: reference(list_identity), arguments: BTreeMap::from([(list_parameter, list)]), } }; } add_calculation(&mut f.project, 7004, list_type, list); eprintln!("analysis depth gate: admitting contextual empty lists through If/Call depth 128"); admit(&f.project, AdmissionLimits::REFERENCE).unwrap(); let mut previous = add_calculation(&mut f.project, 7100, scalar_number(), number(1)); for n in 7101..7132 { previous = add_calculation( &mut f.project, n, scalar_number(), Expr::Call { definition: reference(previous), arguments: BTreeMap::new(), }, ); } eprintln!("analysis depth gate: admitting 32-definition call graph"); admit(&f.project, AdmissionLimits::REFERENCE).unwrap(); add_calculation( &mut f.project, 7132, scalar_number(), Expr::Call { definition: reference(previous), arguments: BTreeMap::new(), }, ); eprintln!("analysis depth gate: rejecting 33-definition call graph"); assert_eq!(reject(&f.project).code, "analysis_call_depth"); } fn boolean_tree(depth: usize) -> Expr { if depth == 0 { Expr::Boolean(true) } else { binary( BinaryOp::Or, boolean_tree(depth - 1), boolean_tree(depth - 1), ) } } #[test] fn stored_closure_limit_aggregates_distinct_called_definitions() { let mut f = fixture(); let boolean = AnalysisType::Scalar(ScalarType::Boolean); let first = add_calculation(&mut f.project, 7200, boolean.clone(), boolean_tree(14)); // 32,767 stored nodes. let second = add_calculation(&mut f.project, 7201, boolean, boolean_tree(14)); admit(&f.project, AdmissionLimits::REFERENCE).unwrap(); // Independent unused definitions each fit. let Some(SharedBody::AnalysisCheck { predicate, .. }) = f.project.elements.get_mut(&f.check).unwrap().shared_mut() else { panic!() }; *predicate = expression(binary( BinaryOp::And, Expr::Call { definition: reference(first), arguments: BTreeMap::new(), }, Expr::Call { definition: reference(second), arguments: BTreeMap::new(), }, )); assert_eq!(reject(&f.project).code, "analysis_nodes"); } #[test] fn static_work_is_lowerable_deterministic_and_aggregated_across_cases() { let f = fixture(); let used = admit(&f.project, AdmissionLimits::REFERENCE) .unwrap() .admission_usage() .static_events; let exact_limit = AdmissionLimits { max_static_events: used, }; let admitted = admit(&f.project, exact_limit).unwrap(); assert_eq!(admitted.admission_limits(), exact_limit); assert_eq!(admitted.admission_usage().static_events, used); assert!( admit( &f.project, AdmissionLimits { max_static_events: used - 1 } ) .is_err() ); assert!( admit( &f.project, AdmissionLimits { max_static_events: 0 } ) .is_err() ); assert!( admit( &f.project, AdmissionLimits { max_static_events: 1_000_001 } ) .is_err() ); let mut more = f.project.clone(); for n in 8000..8010 { more.elements.insert( Id::from_u128(n), shared( Id::from_u128(n), &format!("case_{n}"), SharedBody::AnalysisCase { subject: None, base: BaselineBase::ModelBaseline, overrides: BTreeMap::new(), }, ), ); } assert!(admit(&more, exact_limit).is_err()); let first = admit(&more, AdmissionLimits::REFERENCE) .unwrap() .admission_usage(); let second = admit(&more, AdmissionLimits::REFERENCE) .unwrap() .admission_usage(); assert_eq!(first, second); assert!(first.static_events > used); let mut described = f.project.clone(); described .definitions .get_mut(&f.duration) .unwrap() .description = "x".repeat(1024); assert_eq!( admit(&described, AdmissionLimits::REFERENCE) .unwrap() .admission_usage() .static_events, used + 1024, ); // Duration is the first definition in UUID order. The forbidden final byte // must not be inspected before its complete text span has been charged. described .definitions .get_mut(&f.duration) .unwrap() .description = format!("{}\0", "x".repeat(1023)); let text_limit = AdmissionLimits { max_static_events: 2 + described.definitions[&f.duration].name.len() as u64 + 1023, }; let failure = match admit(&described, text_limit) { Ok(_) => panic!("Text inspection must be precharged"), Err(error) => error, }; assert_eq!(failure.code, "analysis_limit"); assert_eq!(failure.object, f.duration); assert_eq!(failure.field, "description"); } #[test] fn existing_transport_record_limits_count_definitions_parameters_overrides_and_nodes() { let f = fixture(); // 14 graph objects, project header, 2 parameters, 2 override rows, 13 Expr nodes. let limits = language::ParseLimits { max_source_bytes: usize::MAX, max_graph_objects: 14, max_records: 32, max_step_references: usize::MAX, }; limits.check_model(&f.project).unwrap(); assert!( language::ParseLimits { max_records: 31, ..limits } .check_model(&f.project) .is_err() ); assert!( language::ParseLimits { max_graph_objects: 13, ..limits } .check_model(&f.project) .is_err() ); } #[test] fn wire_contract_rejects_unknown_imports_duplicates_missing_fields_and_noncanonical_exact() { let f = fixture(); assert!(serde_json::from_str::(r#"{"Imported":{"package":"test"}}"#).is_err()); let argument = serde_json::to_string(&quantity(1, f.second)).unwrap(); let duplicate = format!( r#"{{"Call":{{"definition":{{"Local":"{}"}},"arguments":{{"{}":{},"{}":{}}}}}}}"#, f.combine, f.a.0, argument, f.a.0, argument ); assert!(serde_json::from_str::(&duplicate).is_err()); let case = format!( r#"{{"AnalysisCase":{{"subject":null,"base":"ModelBaseline","overrides":{{"{}":"Unbound","{}":"Unbound"}}}}}}"#, f.receive_time, f.receive_time ); assert!(serde_json::from_str::(&case).is_err()); for wire in [ r#"{"Number":{"numerator":"02","denominator":"1"}}"#, r#"{"Number":{"numerator":"2","denominator":"2"}}"#, r#"{"Number":{"numerator":"1","denominator":"-1"}}"#, r#"{"Number":{"numerator":"-0","denominator":"1"}}"#, ] { assert!(serde_json::from_str::(wire).is_err()); } let literal = number(-1); let unary = Expr::Unary { op: UnaryOp::Negate, operand: Box::new(number(1)), }; assert_ne!(literal, unary); for node in [literal, unary] { assert_eq!( serde_json::from_slice::(&serde_json::to_vec(&node).unwrap()).unwrap(), node ); } let mut missing = serde_json::to_value(&f.project).unwrap(); missing.as_object_mut().unwrap().remove("definitions"); assert!(persistence::decode(&serde_json::to_vec(&missing).unwrap()).is_err()); let mut malformed = serde_json::to_value(&f.project).unwrap(); malformed["elements"][f.receive_time.to_string()]["body"]["Oa"]["Shared"]["EngineeringProperty"].as_object_mut().unwrap().remove("base"); assert!(persistence::decode(&serde_json::to_vec(&malformed).unwrap()).is_err()); } #[test] fn frozen_schema7_upgrades_without_interpreting_descriptive_metadata() { let mut old = Project::empty(); old.id = Id::from_u128(9000); old.alias = "old_metadata".into(); let data = Id::from_u128(9001); let constraint = Id::from_u128(9002); old.elements.insert( data, shared( data, "old_duration", SharedBody::DataType { definition: DataTypeDefinition::Real { minimum: None, maximum: None, }, default: None, unit: "ms, manually documented".into(), }, ), ); old.elements.insert( constraint, shared( constraint, "old_rule", SharedBody::Constraint { expression: "duration < 2 s; prose remains prose".into(), constrained: vec![data], }, ), ); let expected = old.clone(); for version in [5, 6, 7] { let mut value = serde_json::to_value(&old).unwrap(); value["schema_version"] = version.into(); value.as_object_mut().unwrap().remove("definitions"); let decoded = persistence::decode_with_report(&serde_json::to_vec(&value).unwrap()).unwrap(); assert_eq!(decoded.project, expected); assert_eq!(decoded.migration.unwrap().from_schema, version); assert!(decoded.project.definitions.is_empty()); } let f = fixture(); let mut value = serde_json::to_value(&f.project).unwrap(); value["schema_version"] = 7.into(); value.as_object_mut().unwrap().remove("definitions"); assert!(persistence::decode(&serde_json::to_vec(&value).unwrap()).is_err()); let mut empty_catalog = serde_json::to_value(&old).unwrap(); empty_catalog["schema_version"] = 7.into(); assert!(persistence::decode(&serde_json::to_vec(&empty_catalog).unwrap()).is_err()); } #[test] fn unsupported_transition_and_header_only_review_fail_explicitly() { let f = fixture(); let request = transitions::TransitionRequest::new( transitions::TransitionDirection::OaToSa, vec![f.receive_time], ); assert!(transitions::plan_transition(&f.project, &request).is_err()); let trace = Trace { id: Id::from_u128(9200), alias: "analysis_trace".into(), name: "analysis trace".into(), description: String::new(), oa: f.receive_time, sa: f.dispatch_time, kind: TraceKind::Manual, baseline: None, }; assert!(crate::semantic::semantic_facets(&f.project, &trace, &BTreeMap::new()).is_err()); }