Add save-slice-backed runtime fixtures
This commit is contained in:
parent
09b6514dbf
commit
8ca65cbbfb
12 changed files with 974 additions and 47 deletions
|
|
@ -12,6 +12,7 @@ use crate::{
|
|||
};
|
||||
|
||||
pub const STATE_DUMP_FORMAT_VERSION: u32 = 1;
|
||||
pub const SAVE_SLICE_DOCUMENT_FORMAT_VERSION: u32 = 1;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct RuntimeStateDumpSource {
|
||||
|
|
@ -30,6 +31,27 @@ pub struct RuntimeStateDumpDocument {
|
|||
pub state: RuntimeState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct RuntimeSaveSliceDocumentSource {
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub original_save_filename: Option<String>,
|
||||
#[serde(default)]
|
||||
pub original_save_sha256: Option<String>,
|
||||
#[serde(default)]
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeSaveSliceDocument {
|
||||
pub format_version: u32,
|
||||
pub save_slice_id: String,
|
||||
#[serde(default)]
|
||||
pub source: RuntimeSaveSliceDocumentSource,
|
||||
pub save_slice: SmpLoadedSaveSlice,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RuntimeStateImport {
|
||||
pub import_id: String,
|
||||
|
|
@ -581,6 +603,80 @@ pub fn validate_runtime_state_dump_document(
|
|||
document.state.validate()
|
||||
}
|
||||
|
||||
pub fn validate_runtime_save_slice_document(
|
||||
document: &RuntimeSaveSliceDocument,
|
||||
) -> Result<(), String> {
|
||||
if document.format_version != SAVE_SLICE_DOCUMENT_FORMAT_VERSION {
|
||||
return Err(format!(
|
||||
"unsupported save slice document format_version {} (expected {})",
|
||||
document.format_version, SAVE_SLICE_DOCUMENT_FORMAT_VERSION
|
||||
));
|
||||
}
|
||||
if document.save_slice_id.trim().is_empty() {
|
||||
return Err("save_slice_id must not be empty".to_string());
|
||||
}
|
||||
if document
|
||||
.source
|
||||
.description
|
||||
.as_deref()
|
||||
.is_some_and(|text| text.trim().is_empty())
|
||||
{
|
||||
return Err("save slice source.description must not be empty".to_string());
|
||||
}
|
||||
if document
|
||||
.source
|
||||
.original_save_filename
|
||||
.as_deref()
|
||||
.is_some_and(|text| text.trim().is_empty())
|
||||
{
|
||||
return Err("save slice source.original_save_filename must not be empty".to_string());
|
||||
}
|
||||
if document
|
||||
.source
|
||||
.original_save_sha256
|
||||
.as_deref()
|
||||
.is_some_and(|text| text.trim().is_empty())
|
||||
{
|
||||
return Err("save slice source.original_save_sha256 must not be empty".to_string());
|
||||
}
|
||||
for (index, note) in document.source.notes.iter().enumerate() {
|
||||
if note.trim().is_empty() {
|
||||
return Err(format!(
|
||||
"save slice source.notes[{index}] must not be empty"
|
||||
));
|
||||
}
|
||||
}
|
||||
if document.save_slice.mechanism_family.trim().is_empty() {
|
||||
return Err("save_slice.mechanism_family must not be empty".to_string());
|
||||
}
|
||||
if document.save_slice.mechanism_confidence.trim().is_empty() {
|
||||
return Err("save_slice.mechanism_confidence must not be empty".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_runtime_save_slice_document(
|
||||
path: &Path,
|
||||
) -> Result<RuntimeSaveSliceDocument, Box<dyn std::error::Error>> {
|
||||
let text = std::fs::read_to_string(path)?;
|
||||
let document: RuntimeSaveSliceDocument = serde_json::from_str(&text)?;
|
||||
Ok(document)
|
||||
}
|
||||
|
||||
pub fn save_runtime_save_slice_document(
|
||||
path: &Path,
|
||||
document: &RuntimeSaveSliceDocument,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
validate_runtime_save_slice_document(document)
|
||||
.map_err(|err| format!("invalid runtime save slice document: {err}"))?;
|
||||
let bytes = serde_json::to_vec_pretty(document)?;
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
std::fs::write(path, bytes)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_runtime_state_import(
|
||||
path: &Path,
|
||||
) -> Result<RuntimeStateImport, Box<dyn std::error::Error>> {
|
||||
|
|
@ -607,6 +703,28 @@ pub fn load_runtime_state_import_from_str(
|
|||
});
|
||||
}
|
||||
|
||||
if let Ok(document) = serde_json::from_str::<RuntimeSaveSliceDocument>(text) {
|
||||
validate_runtime_save_slice_document(&document)
|
||||
.map_err(|err| format!("invalid runtime save slice document: {err}"))?;
|
||||
let mut description_parts = Vec::new();
|
||||
if let Some(description) = document.source.description {
|
||||
description_parts.push(description);
|
||||
}
|
||||
if let Some(filename) = document.source.original_save_filename {
|
||||
description_parts.push(format!("source save {filename}"));
|
||||
}
|
||||
let import = project_save_slice_to_runtime_state_import(
|
||||
&document.save_slice,
|
||||
&document.save_slice_id,
|
||||
if description_parts.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(description_parts.join(" | "))
|
||||
},
|
||||
)?;
|
||||
return Ok(import);
|
||||
}
|
||||
|
||||
let state: RuntimeState = serde_json::from_str(text)?;
|
||||
state
|
||||
.validate()
|
||||
|
|
@ -713,6 +831,84 @@ mod tests {
|
|||
assert!(import.description.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_and_roundtrips_save_slice_document() {
|
||||
let document = RuntimeSaveSliceDocument {
|
||||
format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION,
|
||||
save_slice_id: "save-slice-smoke".to_string(),
|
||||
source: RuntimeSaveSliceDocumentSource {
|
||||
description: Some("test save slice".to_string()),
|
||||
original_save_filename: Some("smoke.gms".to_string()),
|
||||
original_save_sha256: Some("deadbeef".to_string()),
|
||||
notes: vec!["captured fixture".to_string()],
|
||||
},
|
||||
save_slice: crate::SmpLoadedSaveSlice {
|
||||
file_extension_hint: Some("gms".to_string()),
|
||||
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
|
||||
mechanism_family: "classic-save-rehydrate-v1".to_string(),
|
||||
mechanism_confidence: "grounded".to_string(),
|
||||
trailer_family: None,
|
||||
bridge_family: None,
|
||||
profile: None,
|
||||
candidate_availability_table: None,
|
||||
special_conditions_table: None,
|
||||
event_runtime_collection: None,
|
||||
notes: vec![],
|
||||
},
|
||||
};
|
||||
assert!(validate_runtime_save_slice_document(&document).is_ok());
|
||||
|
||||
let nonce = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("system time should be after epoch")
|
||||
.as_nanos();
|
||||
let path = std::env::temp_dir().join(format!("rrt-save-slice-doc-{nonce}.json"));
|
||||
save_runtime_save_slice_document(&path, &document).expect("save slice doc should save");
|
||||
let loaded = load_runtime_save_slice_document(&path).expect("save slice doc should load");
|
||||
assert_eq!(document, loaded);
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loads_save_slice_document_as_runtime_state_import() {
|
||||
let text = serde_json::to_string(&RuntimeSaveSliceDocument {
|
||||
format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION,
|
||||
save_slice_id: "save-slice-import".to_string(),
|
||||
source: RuntimeSaveSliceDocumentSource {
|
||||
description: Some("test save slice import".to_string()),
|
||||
original_save_filename: Some("import.gms".to_string()),
|
||||
original_save_sha256: None,
|
||||
notes: vec![],
|
||||
},
|
||||
save_slice: crate::SmpLoadedSaveSlice {
|
||||
file_extension_hint: Some("gms".to_string()),
|
||||
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
|
||||
mechanism_family: "classic-save-rehydrate-v1".to_string(),
|
||||
mechanism_confidence: "grounded".to_string(),
|
||||
trailer_family: None,
|
||||
bridge_family: None,
|
||||
profile: None,
|
||||
candidate_availability_table: None,
|
||||
special_conditions_table: None,
|
||||
event_runtime_collection: None,
|
||||
notes: vec![],
|
||||
},
|
||||
})
|
||||
.expect("save slice doc should serialize");
|
||||
|
||||
let import = load_runtime_state_import_from_str(&text, "fallback")
|
||||
.expect("save slice document should load as runtime import");
|
||||
assert_eq!(import.import_id, "save-slice-import");
|
||||
assert_eq!(
|
||||
import
|
||||
.state
|
||||
.metadata
|
||||
.get("save_slice.import_projection")
|
||||
.map(String::as_str),
|
||||
Some("partial-runtime-restore-v1")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projects_save_slice_into_runtime_state_import() {
|
||||
let save_slice = SmpLoadedSaveSlice {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue