1295 lines
43 KiB
Rust
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());
|
|
}
|