use std::time::Duration;
use oxirs_physics::sync::{
state_diff, BidirectionalSync, BidirectionalSyncConfig, PhysicsState, PhysicsStateValue,
StateGraphConfig, StateToRdfWriter, SyncDirection,
};
fn entity() -> &'static str {
"urn:example:battery:001"
}
fn state_at(step: u64, voltage: f64, temperature: f64) -> PhysicsState {
let mut s = PhysicsState::new(entity());
s.step = step;
s.set_scalar("voltage", voltage);
s.set_scalar("temperature", temperature);
s
}
#[test]
fn full_render_round_trips_through_diff_when_no_changes() {
let writer = StateToRdfWriter::new();
let s = state_at(0, 3.7, 298.15);
let q = writer.render_full(&s);
assert!(q.contains("INSERT DATA"));
assert!(q.contains("phys:State"));
let d = state_diff(&s, &s, 1e-9);
assert!(d.is_empty());
}
#[test]
fn diff_emits_only_changed_property() {
let writer = StateToRdfWriter::new();
let s0 = state_at(0, 3.7, 298.15);
let s1 = state_at(1, 3.95, 298.15);
let q = writer.render_diff(&s0, &s1).expect("non-empty diff");
assert!(q.contains("phys:voltage"));
assert!(!q.contains("phys:temperature"));
}
#[test]
fn writer_supports_default_graph() {
let cfg = StateGraphConfig {
named_graph: None,
..Default::default()
};
let writer = StateToRdfWriter::with_config(cfg);
let q = writer.render_full(&state_at(0, 3.7, 298.15));
assert!(!q.contains("GRAPH <"));
assert!(q.contains("INSERT DATA"));
}
#[test]
fn bidirectional_sync_full_round_trip() {
let mut sync = BidirectionalSync::new(BidirectionalSyncConfig {
min_interval: Duration::from_millis(0),
..Default::default()
});
let s0 = state_at(0, 3.7, 298.15);
let r0 = sync.push_state(&s0).expect("push 0");
assert_eq!(r0.direction, SyncDirection::StateToRdf);
assert!(r0
.sparql
.expect("must produce SPARQL")
.contains("phys:State"));
std::thread::sleep(Duration::from_millis(2));
let s1 = state_at(1, 3.95, 298.15);
let r1 = sync.push_state(&s1).expect("push 1");
assert_eq!(r1.diff.changed.len(), 1);
assert!(r1.diff.changed.contains_key("voltage"));
std::thread::sleep(Duration::from_millis(2));
let r2 = sync
.pull_state(entity(), 2, |entity, step| {
assert_eq!(entity, "urn:example:battery:001");
assert_eq!(step, 2);
Ok(vec![
oxirs_physics::sync::rdf_to_state::RdfPropertyRow {
predicate: "voltage".to_string(),
literal: "4.10".to_string(),
datatype: Some("http://www.w3.org/2001/XMLSchema#double".to_string()),
},
oxirs_physics::sync::rdf_to_state::RdfPropertyRow {
predicate: "temperature".to_string(),
literal: "299.0".to_string(),
datatype: Some("http://www.w3.org/2001/XMLSchema#double".to_string()),
},
])
})
.expect("pull 2");
assert_eq!(r2.direction, SyncDirection::RdfToState);
let pulled = r2.re_extracted.expect("must re-extract");
assert_eq!(pulled.entity_iri, entity());
assert_eq!(pulled.values.len(), 2);
match pulled.values.get("voltage") {
Some(PhysicsStateValue::Scalar(v)) => assert!((*v - 4.10).abs() < 1e-9),
other => panic!("expected scalar voltage, got {other:?}"),
}
}
#[test]
fn min_interval_skips_premature_pushes() {
let mut sync = BidirectionalSync::new(BidirectionalSyncConfig {
min_interval: Duration::from_secs(60), ..Default::default()
});
let s = state_at(0, 3.7, 298.15);
let r0 = sync.push_state(&s).expect("first push");
assert_eq!(r0.direction, SyncDirection::StateToRdf);
let r1 = sync.push_state(&s).expect("second push");
assert_eq!(r1.direction, SyncDirection::Skipped);
assert!(r1.sparql.is_none());
}
#[test]
fn vector_property_round_trips_through_csv_string_encoding() {
let mut s = PhysicsState::new(entity());
s.values.insert(
"velocity".to_string(),
PhysicsStateValue::Vector(vec![1.0, -2.0, 3.5]),
);
let writer = StateToRdfWriter::new();
let full = writer.render_full(&s);
assert!(full.contains("\"1,-2,3.5\""));
let row = oxirs_physics::sync::rdf_to_state::RdfPropertyRow {
predicate: "velocity".to_string(),
literal: "1,-2,3.5".to_string(),
datatype: Some("http://www.w3.org/2001/XMLSchema#string".to_string()),
};
let out = oxirs_physics::sync::RdfToStateExtractor::new()
.extract(entity(), 0, &[row])
.expect("extract ok");
match out.state.values.get("velocity") {
Some(PhysicsStateValue::Vector(v)) => {
assert_eq!(v.as_slice(), &[1.0, -2.0, 3.5]);
}
other => panic!("expected vector, got {other:?}"),
}
}
#[test]
fn empty_diff_after_first_push_produces_no_sparql() {
let mut sync = BidirectionalSync::new(BidirectionalSyncConfig {
min_interval: Duration::from_millis(0),
..Default::default()
});
let s = state_at(0, 3.7, 298.15);
sync.push_state(&s).expect("first push");
std::thread::sleep(Duration::from_millis(2));
let r = sync.push_state(&s).expect("second push, no changes");
assert_eq!(r.direction, SyncDirection::StateToRdf);
assert!(r.sparql.is_none());
assert!(r.diff.is_empty());
}