Files
ARCADIA-Studio/crates/oa-core/src/analysis/tests.rs
T

1295 lines
43 KiB
Rust

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<Id>) -> 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<Id, Binding> {
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::<u8>::number(Exact::from_integer(0));
assert_eq!(
NumericType::<u8>::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::<u8>::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::<DefinitionRef>(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::<Expr>(&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::<SharedBody>(&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::<Expr>(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::<Expr>(&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());
}