use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "PascalCase")]
pub struct SchemaVersion {
pub major: u32,
pub minor: u32,
}
impl Default for SchemaVersion {
fn default() -> Self {
Self { major: 2, minor: 1 }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ComputeSystem {
pub owner: String,
pub schema_version: SchemaVersion,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub hosting_system_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub container: Option<Container>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub virtual_machine: Option<VirtualMachine>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub should_terminate_on_last_handle_closed: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct Container {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub guest_os: Option<GuestOs>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub storage: Option<Storage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub networking: Option<ContainerNetworking>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mapped_directories: Vec<MappedDirectory>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mapped_pipes: Vec<MappedPipe>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub processor: Option<ContainerProcessor>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub memory: Option<ContainerMemory>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct GuestOs {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub host_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct Storage {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub layers: Vec<Layer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct Layer {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub id: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ContainerNetworking {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allow_unqualified_dns_query: Option<bool>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dns_search_list: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub network_shared_container_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct MappedDirectory {
pub host_path: String,
pub container_path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub read_only: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct MappedPipe {
pub host_path: String,
pub container_pipe_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ContainerProcessor {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub count: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub maximum: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub weight: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ContainerMemory {
#[serde(rename = "SizeInMB", default, skip_serializing_if = "Option::is_none")]
pub size_in_mb: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct VirtualMachine {
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub stop_on_reset: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry_changes: Option<RegistryChanges>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chipset: Option<Chipset>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compute_topology: Option<Topology>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub devices: Option<Devices>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub debug_options: Option<DebugOptions>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub guest_state: Option<GuestState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub runtime_state_file_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct RegistryChanges {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub add_values: Vec<RegistryValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct RegistryValue {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key: Option<RegistryKey>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub name: String,
#[serde(rename = "Type", default, skip_serializing_if = "Option::is_none")]
pub r#type: Option<RegistryValueType>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub string_value: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub binary_value: String,
#[serde(
rename = "DWordValue",
default,
skip_serializing_if = "Option::is_none"
)]
pub d_word_value: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct RegistryKey {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hive: Option<RegistryHive>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub name: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RegistryHive {
System,
Software,
Security,
Sam,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RegistryValueType {
None,
String,
ExpandedString,
MultiString,
Binary,
#[serde(rename = "DWord")]
DWord,
#[serde(rename = "QWord")]
QWord,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct Chipset {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub uefi: Option<Uefi>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct Uefi {
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub enable_debugger: bool,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub apply_secure_boot_template: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub secure_boot_template_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub boot_this: Option<UefiBootEntry>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub console: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct UefiBootEntry {
pub device_type: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub device_path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disk_number: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct Topology {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub memory: Option<TopologyMemory>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub processor: Option<TopologyProcessor>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct TopologyMemory {
#[serde(rename = "SizeInMB")]
pub size_in_mb: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allow_overcommit: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enable_hot_hint: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct TopologyProcessor {
pub count: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ComPort {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub named_pipe: String,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub optimize_for_debugger: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct Devices {
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub com_ports: BTreeMap<String, ComPort>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub scsi: BTreeMap<String, ScsiController>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub virtual_smb: Option<VirtualSmb>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gpu: Option<GpuAssignment>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hv_socket: Option<HvSocket2>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub guest_crash_reporting: Option<GuestCrashReporting>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct DebugOptions {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub bugcheck_saved_state_file_name: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub bugcheck_no_crashdump_saved_state_file_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct GuestCrashReporting {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub windows_crash_settings: Option<WindowsCrashReporting>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct WindowsCrashReporting {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub dump_file_name: String,
#[serde(default, skip_serializing_if = "is_zero_i64")]
pub max_dump_size: i64,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub dump_type: String,
}
#[allow(clippy::trivially_copy_pass_by_ref)] fn is_zero_i64(v: &i64) -> bool {
*v == 0
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct HvSocketServiceConfig {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub bind_security_descriptor: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub connect_security_descriptor: String,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub allow_wildcard_binds: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub disabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct HvSocketSystemConfig {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub default_bind_security_descriptor: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub default_connect_security_descriptor: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub service_table: BTreeMap<String, HvSocketServiceConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct HvSocket2 {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hv_socket_config: Option<HvSocketSystemConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "PascalCase")]
pub struct GpuAssignment {
pub assignment_mode: GpuAssignmentMode,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub assignment_request: Vec<GpuAssignmentRequest>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allow_vendor_extension: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum GpuAssignmentMode {
Default,
List,
Disabled,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "PascalCase")]
pub struct GpuAssignmentRequest {
pub virtual_machine_id_string: String,
pub adapter_luid_high_part: u32,
pub adapter_luid_low_part: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ScsiController {
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub attachments: BTreeMap<String, ScsiAttachment>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ScsiAttachment {
pub path: String,
#[serde(rename = "Type")]
pub r#type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub read_only: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct VirtualSmb {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub shares: Vec<VirtualSmbShare>,
#[serde(
rename = "DirectFileMappingInMB",
default,
skip_serializing_if = "Option::is_none"
)]
pub direct_file_mapping_in_mb: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct VirtualSmbShare {
pub name: String,
pub path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub flags: Option<u32>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub allowed_files: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub options: Option<VirtualSmbShareOptions>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "PascalCase")]
#[allow(clippy::struct_excessive_bools)] pub struct VirtualSmbShareOptions {
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub read_only: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub share_read: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub cache_io: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub no_oplocks: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub take_backup_privilege: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub use_share_root_identity: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub no_directmap: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub no_locks: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub no_dirnotify: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub vm_shared_memory: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub restrict_file_access: bool,
#[serde(
default,
skip_serializing_if = "std::ops::Not::not",
rename = "ForceLevelIIOplocks"
)]
pub force_level_ii_oplocks: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub reparse_base_layer: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub pseudo_oplocks: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub non_cache_io: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub pseudo_dirnotify: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub single_file_mapping: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct GuestState {
pub guest_state_file_path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ProcessParameters {
pub command_line: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub working_directory: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub environment: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub emulate_console: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub create_std_in_pipe: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub create_std_out_pipe: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub create_std_err_pipe: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub console_size: Option<ConsoleSize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ConsoleSize {
pub height: u16,
pub width: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ProcessStatus {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub process_id: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exit_code: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_wait_result: Option<i32>,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct Statistics {
#[serde(default)]
pub timestamp: Option<String>,
#[serde(default)]
pub container_start_time: Option<String>,
#[serde(default, rename = "Uptime100ns")]
pub uptime_100ns: u64,
#[serde(default)]
pub processor: Option<ProcessorStats>,
#[serde(default)]
pub memory: Option<MemoryStats>,
#[serde(default)]
pub storage: Option<StorageStats>,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ProcessorStats {
#[serde(default, rename = "TotalRuntime100ns")]
pub total_runtime_100ns: u64,
#[serde(default, rename = "RuntimeUser100ns")]
pub runtime_user_100ns: u64,
#[serde(default, rename = "RuntimeKernel100ns")]
pub runtime_kernel_100ns: u64,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct MemoryStats {
#[serde(default)]
pub memory_usage_commit_bytes: u64,
#[serde(default)]
pub memory_usage_commit_peak_bytes: u64,
#[serde(default)]
pub memory_usage_private_working_set_bytes: u64,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct StorageStats {
#[serde(default)]
pub read_count_normalized: u64,
#[serde(default)]
pub read_size_bytes: u64,
#[serde(default)]
pub write_count_normalized: u64,
#[serde(default)]
pub write_size_bytes: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ModifySettingRequest {
pub resource_path: String,
pub request_type: ModifyRequestType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub settings: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub guest_request: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
pub enum ModifyRequestType {
#[default]
Add,
Remove,
Update,
}
#[cfg(test)]
mod tests {
use super::{
ComputeSystem, Container, ContainerMemory, ContainerProcessor, GuestOs, Layer,
ModifyRequestType, ModifySettingRequest, SchemaVersion, Statistics, Storage,
};
#[test]
fn schema_version_default_is_v2_1() {
let v = SchemaVersion::default();
assert_eq!(v.major, 2);
assert_eq!(v.minor, 1);
}
#[test]
fn compute_system_json_round_trip_container() {
let doc = ComputeSystem {
owner: "zlayer".to_string(),
schema_version: SchemaVersion::default(),
hosting_system_id: String::new(),
container: Some(Container {
storage: Some(Storage {
layers: vec![Layer {
id: "0f2c0c2a-1111-2222-3333-444455556666".to_string(),
path: r"C:\ProgramData\zlayer\layers\base".to_string(),
}],
path: Some(r"C:\ProgramData\zlayer\scratch\abc".to_string()),
}),
networking: None,
mapped_directories: Vec::new(),
mapped_pipes: Vec::new(),
guest_os: Some(GuestOs {
host_name: Some("test-host".to_string()),
}),
processor: Some(ContainerProcessor {
count: Some(2),
maximum: None,
weight: None,
}),
memory: Some(ContainerMemory {
size_in_mb: Some(1024),
}),
}),
virtual_machine: None,
should_terminate_on_last_handle_closed: None,
};
let json = serde_json::to_string(&doc).expect("serialize");
assert!(json.contains("\"Owner\":\"zlayer\""));
assert!(json.contains("\"SchemaVersion\":{\"Major\":2,\"Minor\":1}"));
assert!(json.contains("\"GuestOs\":{\"HostName\":\"test-host\"}"));
assert!(json.contains("\"SizeInMB\":1024"));
let back: ComputeSystem = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.owner, "zlayer");
assert_eq!(back.schema_version, SchemaVersion { major: 2, minor: 1 });
let container = back.container.expect("container present");
let storage = container.storage.expect("storage present");
assert_eq!(storage.layers.len(), 1);
assert_eq!(storage.layers[0].id, "0f2c0c2a-1111-2222-3333-444455556666");
assert_eq!(
storage.path.as_deref(),
Some(r"C:\ProgramData\zlayer\scratch\abc"),
);
assert_eq!(
container.guest_os.and_then(|g| g.host_name).as_deref(),
Some("test-host"),
);
assert_eq!(container.processor.and_then(|p| p.count), Some(2));
assert_eq!(container.memory.and_then(|m| m.size_in_mb), Some(1024));
}
#[test]
fn empty_container_omits_all_optional_fields() {
let doc = ComputeSystem {
owner: "zlayer".to_string(),
schema_version: SchemaVersion::default(),
hosting_system_id: String::new(),
container: Some(Container::default()),
virtual_machine: None,
should_terminate_on_last_handle_closed: None,
};
let json = serde_json::to_string(&doc).expect("serialize");
assert!(
json.contains("\"Container\":{}"),
"empty Container must serialize to {{}}, got: {json}",
);
assert!(!json.contains("null"), "no null fields: {json}");
assert!(!json.contains("[]"), "no empty arrays: {json}");
assert!(!json.contains("\"\""), "no empty strings: {json}");
for key in [
"GuestOs",
"Storage",
"Networking",
"MappedDirectories",
"MappedPipes",
"Processor",
"Memory",
"HostingSystemId",
"VirtualMachine",
"ShouldTerminateOnLastHandleClosed",
] {
assert!(
!json.contains(&format!("\"{key}\"")),
"key {key} must be omitted, got: {json}",
);
}
}
#[test]
fn layer_serializes_id_not_uppercase_id() {
let layer = Layer {
id: "0f2c0c2a-1111-2222-3333-444455556666".to_string(),
path: r"C:\layers\base".to_string(),
};
let json = serde_json::to_string(&layer).expect("serialize layer");
assert!(json.contains("\"Id\":"), "must emit `Id`: {json}");
assert!(!json.contains("\"ID\":"), "must NOT emit `ID`: {json}");
assert!(json.contains("\"Path\":"), "must emit `Path`: {json}");
let empty = serde_json::to_string(&Layer::default()).expect("serialize");
assert_eq!(
empty, "{}",
"empty Layer must serialize to {{}}, got: {empty}"
);
}
#[test]
fn statistics_parses_sample_json() {
let payload = r#"{"Timestamp":"2026-04-21T12:34:56Z","Uptime100ns":2960000000,"Processor":{"TotalRuntime100ns":1234567,"RuntimeUser100ns":900000,"RuntimeKernel100ns":334567},"Memory":{"MemoryUsageCommitBytes":268435456,"MemoryUsageCommitPeakBytes":314572800,"MemoryUsagePrivateWorkingSetBytes":201326592},"Storage":{"ReadCountNormalized":42,"ReadSizeBytes":1048576,"WriteCountNormalized":13,"WriteSizeBytes":262144}}"#;
let stats: Statistics = serde_json::from_str(payload).expect("parse statistics");
assert_eq!(stats.timestamp.as_deref(), Some("2026-04-21T12:34:56Z"));
assert_eq!(stats.uptime_100ns, 2_960_000_000);
let cpu = stats.processor.expect("processor");
assert_eq!(cpu.total_runtime_100ns, 1_234_567);
assert_eq!(cpu.runtime_user_100ns, 900_000);
assert_eq!(cpu.runtime_kernel_100ns, 334_567);
let mem = stats.memory.expect("memory");
assert_eq!(mem.memory_usage_commit_bytes, 268_435_456);
assert_eq!(mem.memory_usage_commit_peak_bytes, 314_572_800);
assert_eq!(mem.memory_usage_private_working_set_bytes, 201_326_592);
let storage = stats.storage.expect("storage");
assert_eq!(storage.read_count_normalized, 42);
assert_eq!(storage.read_size_bytes, 1_048_576);
assert_eq!(storage.write_count_normalized, 13);
assert_eq!(storage.write_size_bytes, 262_144);
}
#[test]
fn modify_setting_request_round_trip() {
let req = ModifySettingRequest {
resource_path: "VirtualMachine/Devices/VirtualSmb/Shares".to_string(),
request_type: ModifyRequestType::Add,
settings: Some(serde_json::json!({"Name": "layer1", "Path": "C:\\foo"})),
guest_request: None,
};
let s = serde_json::to_string(&req).unwrap();
assert!(s.contains("\"ResourcePath\""));
assert!(s.contains("\"RequestType\":\"Add\""));
assert!(s.contains("\"Settings\""));
assert!(!s.contains("\"GuestRequest\""));
let _back: ModifySettingRequest = serde_json::from_str(&s).unwrap();
}
}