use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};
use physics_in_parallel::math::{Dense, Tensor};
use scientific_workflow::system_state::{FieldSpec, StateError, StateSpec, SystemState, TimePoint};
const STATE_TEMPLATE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/state.json");
#[test]
fn tensor_state_round_trip_integrates_public_modules() {
let specification =
StateSpec::load(STATE_TEMPLATE).expect("canonical state template must load");
assert_eq!(specification.source().to_string_lossy(), STATE_TEMPLATE);
assert_eq!(specification.len(), 3);
assert!(!specification.is_empty());
let fields: &[FieldSpec] = specification.fields();
assert_eq!(fields[0].index(), 0);
assert_eq!(fields[0].name(), "population");
assert_eq!(
fields[0].type_tag(),
"physics_in_parallel.tensor.dense.u64.v1"
);
assert_eq!(fields[1].index(), 1);
assert_eq!(fields[1].name(), "space");
assert_eq!(
fields[1].type_tag(),
"physics_in_parallel.tensor.dense.u64.v1"
);
assert_eq!(fields[2].index(), 2);
assert_eq!(fields[2].name(), "activity");
assert_eq!(
fields[2].type_tag(),
"physics_in_parallel.tensor.dense.u8.v1"
);
assert!(specification.contains("population"));
assert_eq!(
specification.get("space").expect("space field must exist"),
&fields[1]
);
let original_json: serde_json::Value = serde_json::from_slice(
&fs::read(STATE_TEMPLATE).expect("canonical state template must be readable"),
)
.expect("canonical state template must contain valid JSON");
let serialized = specification
.to_json()
.expect("validated specification must serialize");
let serialized_json: serde_json::Value =
serde_json::from_str(&serialized).expect("serialized template must be valid JSON");
assert_eq!(serialized_json, original_json);
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock must be after the Unix epoch")
.as_nanos();
let round_trip_directory = std::env::temp_dir().join(format!(
"scientific-workflow-public-system-state-{}-{nonce}",
std::process::id()
));
let round_trip_path = round_trip_directory.join("state.json");
fs::create_dir_all(&round_trip_directory)
.expect("temporary round-trip directory must be created");
fs::write(&round_trip_path, serialized).expect("round-trip template must be written");
let restored =
StateSpec::load(&round_trip_path).expect("round-trip template must load successfully");
assert_eq!(restored.source(), round_trip_path);
assert_eq!(restored.fields(), specification.fields());
let initial_time =
TimePoint::from_physical(0, 0.25).expect("finite physical time must be accepted");
let mut state: SystemState = specification.empty(initial_time);
assert_eq!(state.time().index(), 0);
assert_eq!(state.time().physical(), Some(0.25));
assert_eq!(state.len(), 3);
assert_eq!(state.loaded(), 0);
assert!(state.is_blank());
assert!(!state.has("population").expect("field must be declared"));
assert!(matches!(
state.get::<Tensor<u64, Dense>>("population"),
Err(StateError::MissingValue { ref field }) if field == "population"
));
assert!(matches!(
state.get::<Tensor<u64, Dense>>("temperature"),
Err(StateError::UnknownField { ref field }) if field == "temperature"
));
let mut population = Tensor::<u64, Dense>::zeros(&[3]);
population.set(&[0], 10);
population.set(&[1], 20);
population.set(&[2], 30);
let mut space = Tensor::<u64, Dense>::zeros(&[2, 2]);
space.set(&[0, 0], 1);
space.set(&[0, 1], 2);
space.set(&[1, 0], 3);
space.set(&[1, 1], 4);
let mut activity = Tensor::<u8, Dense>::zeros(&[3]);
activity.set(&[0], 1);
activity.set(&[1], 0);
activity.set(&[2], 1);
state
.set("population", population)
.expect("population tensor must move into its declared slot");
state
.set("space", space)
.expect("space tensor must move into its declared slot");
state
.set("activity", activity)
.expect("activity tensor must move into its declared slot");
assert_eq!(state.loaded(), 3);
assert!(!state.is_blank());
assert!(
state
.is::<Tensor<u64, Dense>>("population")
.expect("field must be declared")
);
assert_eq!(
state
.get::<Tensor<u64, Dense>>("space")
.expect("space tensor type must match")
.shape(),
&[2, 2]
);
state
.get_mut::<Tensor<u64, Dense>>("population")
.expect("population tensor type must match")
.set(&[1], 21);
assert_eq!(
state
.get::<Tensor<u64, Dense>>("population")
.expect("population tensor must remain available")
.get(&[1]),
21
);
let mismatch = state
.get::<Tensor<u64, Dense>>("activity")
.expect_err("activity stores a u8 tensor, not a u64 tensor");
assert!(matches!(
mismatch,
StateError::TypeMismatch {
ref field,
expected,
actual,
} if field == "activity"
&& expected == std::any::type_name::<Tensor<u64, Dense>>()
&& actual == std::any::type_name::<Tensor<u8, Dense>>()
));
assert_eq!(
state
.get::<Tensor<u8, Dense>>("activity")
.expect("failed borrow must preserve activity tensor")
.get(&[2]),
1
);
let mut branch = state.clone();
branch
.get_mut::<Tensor<u64, Dense>>("space")
.expect("cloned space tensor type must match")
.set(&[0, 0], 99);
assert_eq!(
branch
.get::<Tensor<u64, Dense>>("space")
.expect("cloned space tensor must remain available")
.get(&[0, 0]),
99
);
assert_eq!(
state
.get::<Tensor<u64, Dense>>("space")
.expect("original space tensor must remain available")
.get(&[0, 0]),
1
);
let population = state
.take::<Tensor<u64, Dense>>("population")
.expect("population tensor must move out of the state");
let space = state
.take::<Tensor<u64, Dense>>("space")
.expect("space tensor must move out of the state");
let activity = state
.take::<Tensor<u8, Dense>>("activity")
.expect("activity tensor must move out of the state");
assert_eq!(population.shape(), &[3]);
assert_eq!(population.get(&[0]), 10);
assert_eq!(population.get(&[1]), 21);
assert_eq!(population.get(&[2]), 30);
assert_eq!(space.shape(), &[2, 2]);
assert_eq!(space.get(&[1, 1]), 4);
assert_eq!(activity.shape(), &[3]);
assert_eq!(activity.get(&[0]), 1);
assert_eq!(activity.get(&[1]), 0);
assert_eq!(activity.get(&[2]), 1);
assert_eq!(state.loaded(), 0);
assert!(state.is_blank());
let later = state.empty(TimePoint::new(1));
assert_eq!(later.time().index(), 1);
assert_eq!(later.time().physical(), None);
assert!(later.is_blank());
assert_eq!(later.fields(), state.fields());
assert!(std::ptr::eq(later.spec().fields(), state.spec().fields()));
fs::remove_dir_all(round_trip_directory)
.expect("temporary round-trip directory must be removed");
}