Die gemeinsame Plattform ist jetzt softwareseitig deutlich vollständiger. Der Host-Core hat mit [show_store.rs](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/crates/infinity_host/src/show_store.rs>) eine echte Runtime-Bibliothek und Persistenz für aktive Szene, Runtime-Presets, Runtime-Gruppen und kreative Varianten bekommen; die Simulation in [simulation.rs](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/crates/infinity_host/src/simulation.rs>) liefert jetzt typisierte Command-Ergebnisse, saubere Fehlercodes und persistiert nach data/runtime_state.json. Dazu kommt das generische External-Show-Control-Interface in [external_control.rs](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/crates/infinity_host/src/external_control.rs>), damit spätere Adapter nur auf definierte Commands und Snapshot-/Preset-/Parameter-Flächen zugreifen.
Die API v1 ist als Produktgrenze geschärft in [dto.rs](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/crates/infinity_host_api/src/dto.rs>) und [server.rs](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/crates/infinity_host_api/src/server.rs>): getrennte Modelle für `state`, `preview`, `snapshot`, `command response`, `event stream` und stabile Fehlerobjekte mit echten Codes statt generischem Fallback. Dazu kamen `GET /api/v1/state` und `GET /api/v1/preview`, neue persistenzbezogene Commands wie `save_preset`, `save_creative_snapshot`, `recall_creative_snapshot`, `set_transition_style` und `upsert_group`, plus serverseitige Durchreichung der echten Fehlercodes. Die kreative Web-UI in [index.html](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/web/v1/index.html>), [app.js](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/web/v1/app.js>) und [styles.css](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/web/v1/styles.css>) nutzt jetzt genau diese API für Preset-Speichern/Überschreiben, Varianten, Transition-Style, filterbaren Eventfeed und klarere Preview-Darstellung, ohne Parallelarchitektur. Die Doku ist auf den neuen Stand gezogen in [docs/host_api.md](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/docs/host_api.md>), [README.md](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/README.md>), [docs/build_and_deploy.md](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/docs/build_and_deploy.md>) und [docs/architecture.md](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/docs/architecture.md>). Verifiziert habe ich `cargo check -q` und `cargo test -q`; dabei laufen die erweiterten Contract- und Persistenztests in [contract.rs](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/crates/infinity_host_api/tests/contract.rs>) sowie neue Core-Tests in [show_store.rs](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/crates/infinity_host/src/show_store.rs>) und [simulation.rs](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/crates/infinity_host/src/simulation.rs>). Nicht separat verifiziert habe ich einen echten Browserlauf der Web-UI; die JS-Datei wurde hier nicht mit `node` geprüft, weil `node` in dieser Umgebung nicht installiert ist.
This commit is contained in:
@@ -5,8 +5,9 @@ use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{Shutdown, SocketAddr, TcpStream};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
fn sample_project() -> ProjectConfig {
|
||||
ProjectConfig::from_toml_str(include_str!("../../../config/project.example.toml"))
|
||||
@@ -14,7 +15,15 @@ fn sample_project() -> ProjectConfig {
|
||||
}
|
||||
|
||||
fn start_server() -> HostApiServer {
|
||||
let service: Arc<dyn HostApiPort> = SimulationHostService::spawn_shared(sample_project());
|
||||
let service: Arc<dyn HostApiPort> = Arc::new(SimulationHostService::new(sample_project()));
|
||||
HostApiServer::bind("127.0.0.1:0", service).expect("server must bind")
|
||||
}
|
||||
|
||||
fn start_server_with_runtime_state(path: &PathBuf) -> HostApiServer {
|
||||
let service: Arc<dyn HostApiPort> = Arc::new(
|
||||
SimulationHostService::try_new_with_persistence(sample_project(), path)
|
||||
.expect("persistent service must initialize"),
|
||||
);
|
||||
HostApiServer::bind("127.0.0.1:0", service).expect("server must bind")
|
||||
}
|
||||
|
||||
@@ -25,118 +34,183 @@ struct HttpResponse {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_serves_creative_console_shell() {
|
||||
fn root_and_web_assets_target_the_versioned_api_contract() {
|
||||
let server = start_server();
|
||||
let response = send_http_request(server.local_addr(), "GET", "/", None);
|
||||
let html = send_http_request(server.local_addr(), "GET", "/", None);
|
||||
let app_js = send_http_request(server.local_addr(), "GET", "/app.js", None);
|
||||
|
||||
assert_eq!(response.status_code, 200);
|
||||
assert!(response
|
||||
assert_eq!(html.status_code, 200);
|
||||
assert!(html
|
||||
.headers
|
||||
.get("content-type")
|
||||
.expect("content-type header")
|
||||
.starts_with("text/html"));
|
||||
assert!(response.body.contains("Infinity Vis / Creative Surface"));
|
||||
assert!(response.body.contains("/app.js"));
|
||||
assert!(html.body.contains("Preset Capture"));
|
||||
assert!(html.body.contains("Creative Snapshots"));
|
||||
assert!(html.body.contains("Event Stream"));
|
||||
|
||||
assert_eq!(app_js.status_code, 200);
|
||||
assert!(app_js.body.contains("/api/v1/state"));
|
||||
assert!(app_js.body.contains("/api/v1/preview"));
|
||||
assert!(app_js.body.contains("save_preset"));
|
||||
assert!(app_js.body.contains("save_creative_snapshot"));
|
||||
|
||||
server.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_endpoint_is_versioned_and_separates_state_and_preview() {
|
||||
fn state_preview_and_snapshot_endpoints_are_versioned_and_separated() {
|
||||
let server = start_server();
|
||||
let response = send_http_request(server.local_addr(), "GET", "/api/v1/snapshot", None);
|
||||
let body: Value = serde_json::from_str(&response.body).expect("snapshot json must parse");
|
||||
|
||||
assert_eq!(response.status_code, 200);
|
||||
assert_eq!(body["api_version"], "v1");
|
||||
assert_eq!(body["state"]["system"]["project_name"], "Infinity Vis");
|
||||
assert_eq!(body["state"]["nodes"].as_array().map(Vec::len), Some(6));
|
||||
assert_eq!(body["preview"]["panels"].as_array().map(Vec::len), Some(18));
|
||||
assert!(body["state"]["active_scene"]["pattern_id"].is_string());
|
||||
|
||||
server.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn catalog_presets_and_groups_endpoints_return_expected_lists() {
|
||||
let server = start_server();
|
||||
|
||||
let catalog = send_http_request(server.local_addr(), "GET", "/api/v1/catalog", None);
|
||||
let presets = send_http_request(server.local_addr(), "GET", "/api/v1/presets", None);
|
||||
let groups = send_http_request(server.local_addr(), "GET", "/api/v1/groups", None);
|
||||
|
||||
let catalog_body: Value = serde_json::from_str(&catalog.body).expect("catalog json");
|
||||
let preset_body: Value = serde_json::from_str(&presets.body).expect("preset json");
|
||||
let group_body: Value = serde_json::from_str(&groups.body).expect("group json");
|
||||
|
||||
assert_eq!(catalog.status_code, 200);
|
||||
assert!(catalog_body["patterns"]
|
||||
.as_array()
|
||||
.expect("patterns array")
|
||||
.iter()
|
||||
.any(|pattern| pattern["pattern_id"] == "walking_pixel"));
|
||||
assert!(preset_body["presets"]
|
||||
.as_array()
|
||||
.expect("presets array")
|
||||
.iter()
|
||||
.any(|preset| preset["preset_id"] == "ocean_gradient"));
|
||||
assert!(group_body["groups"]
|
||||
.as_array()
|
||||
.expect("groups array")
|
||||
.iter()
|
||||
.any(|group| group["group_id"] == "top_panels"));
|
||||
|
||||
server.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_endpoint_applies_state_changes_and_rejects_invalid_payload() {
|
||||
let server = start_server();
|
||||
|
||||
let response = send_http_request(
|
||||
server.local_addr(),
|
||||
"POST",
|
||||
"/api/v1/command",
|
||||
Some(
|
||||
r#"{
|
||||
"request_id": "contract-blackout",
|
||||
"command": {
|
||||
"type": "set_blackout",
|
||||
"payload": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
),
|
||||
);
|
||||
let response_body: Value =
|
||||
serde_json::from_str(&response.body).expect("command response must parse");
|
||||
let state = send_http_request(server.local_addr(), "GET", "/api/v1/state", None);
|
||||
let preview = send_http_request(server.local_addr(), "GET", "/api/v1/preview", None);
|
||||
let snapshot = send_http_request(server.local_addr(), "GET", "/api/v1/snapshot", None);
|
||||
|
||||
let state_body: Value = serde_json::from_str(&state.body).expect("state json");
|
||||
let preview_body: Value = serde_json::from_str(&preview.body).expect("preview json");
|
||||
let snapshot_body: Value = serde_json::from_str(&snapshot.body).expect("snapshot json");
|
||||
|
||||
assert_eq!(response.status_code, 200);
|
||||
assert_eq!(response_body["accepted"], true);
|
||||
assert_eq!(response_body["request_id"], "contract-blackout");
|
||||
assert_eq!(snapshot_body["state"]["global"]["blackout"], true);
|
||||
assert_eq!(state.status_code, 200);
|
||||
assert_eq!(state_body["api_version"], "v1");
|
||||
assert!(state_body.get("state").is_some());
|
||||
assert!(state_body.get("preview").is_none());
|
||||
assert_eq!(state_body["state"]["nodes"].as_array().map(Vec::len), Some(6));
|
||||
|
||||
let invalid = send_http_request(
|
||||
server.local_addr(),
|
||||
"POST",
|
||||
"/api/v1/command",
|
||||
Some(r#"{"command":{"type":"set_blackout","payload":{}}}"#),
|
||||
);
|
||||
let invalid_body: Value =
|
||||
serde_json::from_str(&invalid.body).expect("invalid response must parse");
|
||||
assert_eq!(preview.status_code, 200);
|
||||
assert_eq!(preview_body["api_version"], "v1");
|
||||
assert!(preview_body.get("preview").is_some());
|
||||
assert!(preview_body.get("state").is_none());
|
||||
assert_eq!(preview_body["preview"]["panels"].as_array().map(Vec::len), Some(18));
|
||||
|
||||
assert_eq!(invalid.status_code, 400);
|
||||
assert_eq!(invalid_body["api_version"], "v1");
|
||||
assert_eq!(invalid_body["error"]["code"], "invalid_command");
|
||||
assert_eq!(snapshot.status_code, 200);
|
||||
assert_eq!(snapshot_body["api_version"], "v1");
|
||||
assert!(snapshot_body.get("state").is_some());
|
||||
assert!(snapshot_body.get("preview").is_some());
|
||||
|
||||
server.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn websocket_stream_emits_snapshot_preview_and_event_messages() {
|
||||
fn command_flow_updates_group_parameters_transition_and_blackout() {
|
||||
let server = start_server();
|
||||
|
||||
let responses = [
|
||||
send_command_json(
|
||||
server.local_addr(),
|
||||
r#"{"command":{"type":"select_group","payload":{"group_id":"top_panels"}}}"#,
|
||||
),
|
||||
send_command_json(
|
||||
server.local_addr(),
|
||||
r#"{"command":{"type":"set_scene_parameter","payload":{"key":"speed","value":{"kind":"scalar","value":2.25}}}}"#,
|
||||
),
|
||||
send_command_json(
|
||||
server.local_addr(),
|
||||
r#"{"command":{"type":"set_transition_style","payload":{"style":"chase"}}}"#,
|
||||
),
|
||||
send_command_json(
|
||||
server.local_addr(),
|
||||
r#"{"command":{"type":"set_transition_duration_ms","payload":{"duration_ms":320}}}"#,
|
||||
),
|
||||
send_command_json(
|
||||
server.local_addr(),
|
||||
r#"{"command":{"type":"select_pattern","payload":{"pattern_id":"gradient"}}}"#,
|
||||
),
|
||||
];
|
||||
|
||||
for response in responses {
|
||||
assert_eq!(response.status_code, 200);
|
||||
}
|
||||
|
||||
let state = send_http_request(server.local_addr(), "GET", "/api/v1/state", None);
|
||||
let state_body: Value = serde_json::from_str(&state.body).expect("state json");
|
||||
assert_eq!(state_body["state"]["global"]["selected_group"], "top_panels");
|
||||
assert_eq!(state_body["state"]["global"]["transition_style"], "chase");
|
||||
assert_eq!(state_body["state"]["global"]["transition_duration_ms"], 320);
|
||||
assert_eq!(state_body["state"]["active_scene"]["pattern_id"], "gradient");
|
||||
assert!(state_body["state"]["active_scene"]["parameters"]
|
||||
.as_array()
|
||||
.expect("parameter array")
|
||||
.iter()
|
||||
.any(|parameter| parameter["key"] == "speed" && parameter["value"]["value"] == 2.25));
|
||||
|
||||
let blackout = send_command_json(
|
||||
server.local_addr(),
|
||||
r#"{"command":{"type":"set_blackout","payload":{"enabled":true}}}"#,
|
||||
);
|
||||
let blackout_body: Value = serde_json::from_str(&blackout.body).expect("blackout json");
|
||||
assert_eq!(blackout.status_code, 200);
|
||||
assert_eq!(blackout_body["command_type"], "set_blackout");
|
||||
|
||||
let preview = send_http_request(server.local_addr(), "GET", "/api/v1/preview", None);
|
||||
let preview_body: Value = serde_json::from_str(&preview.body).expect("preview json");
|
||||
assert!(preview_body["preview"]["panels"]
|
||||
.as_array()
|
||||
.expect("preview panels")
|
||||
.iter()
|
||||
.all(|panel| panel["energy_percent"] == 0 && panel["source"] == "blackout"));
|
||||
|
||||
server.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presets_and_creative_snapshots_persist_across_restart() {
|
||||
let runtime_state_path = unique_runtime_state_path("persistence");
|
||||
let server = start_server_with_runtime_state(&runtime_state_path);
|
||||
|
||||
let _ = send_command_json(
|
||||
server.local_addr(),
|
||||
r#"{"command":{"type":"select_group","payload":{"group_id":"bottom_panels"}}}"#,
|
||||
);
|
||||
let _ = send_command_json(
|
||||
server.local_addr(),
|
||||
r#"{"command":{"type":"select_pattern","payload":{"pattern_id":"noise"}}}"#,
|
||||
);
|
||||
let _ = send_command_json(
|
||||
server.local_addr(),
|
||||
r#"{"command":{"type":"set_scene_parameter","payload":{"key":"grain","value":{"kind":"scalar","value":0.93}}}}"#,
|
||||
);
|
||||
let save_preset = send_command_json(
|
||||
server.local_addr(),
|
||||
r#"{"command":{"type":"save_preset","payload":{"preset_id":"user_noise_floor","overwrite":false}}}"#,
|
||||
);
|
||||
let save_snapshot = send_command_json(
|
||||
server.local_addr(),
|
||||
r#"{"command":{"type":"save_creative_snapshot","payload":{"snapshot_id":"variant_floor","label":"Variant Floor","overwrite":false}}}"#,
|
||||
);
|
||||
assert_eq!(save_preset.status_code, 200);
|
||||
assert_eq!(save_snapshot.status_code, 200);
|
||||
server.shutdown();
|
||||
|
||||
let restarted = start_server_with_runtime_state(&runtime_state_path);
|
||||
let catalog = send_http_request(restarted.local_addr(), "GET", "/api/v1/catalog", None);
|
||||
let state = send_http_request(restarted.local_addr(), "GET", "/api/v1/state", None);
|
||||
|
||||
let catalog_body: Value = serde_json::from_str(&catalog.body).expect("catalog json");
|
||||
let state_body: Value = serde_json::from_str(&state.body).expect("state json");
|
||||
|
||||
assert!(catalog_body["presets"]
|
||||
.as_array()
|
||||
.expect("preset array")
|
||||
.iter()
|
||||
.any(|preset| preset["preset_id"] == "user_noise_floor" && preset["source"] == "runtime_user"));
|
||||
assert!(catalog_body["creative_snapshots"]
|
||||
.as_array()
|
||||
.expect("snapshot array")
|
||||
.iter()
|
||||
.any(|snapshot| snapshot["snapshot_id"] == "variant_floor"));
|
||||
assert_eq!(state_body["state"]["active_scene"]["pattern_id"], "noise");
|
||||
assert_eq!(state_body["state"]["active_scene"]["target_group"], "bottom_panels");
|
||||
assert!(state_body["state"]["active_scene"]["parameters"]
|
||||
.as_array()
|
||||
.expect("parameter array")
|
||||
.iter()
|
||||
.any(|parameter| parameter["key"] == "grain" && parameter["value"]["value"] == 0.93));
|
||||
|
||||
restarted.shutdown();
|
||||
let _ = std::fs::remove_file(runtime_state_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn websocket_stream_reports_event_codes_and_command_failures_stay_typed() {
|
||||
let server = start_server();
|
||||
let mut stream = open_websocket(server.local_addr());
|
||||
|
||||
@@ -148,43 +222,97 @@ fn websocket_stream_emits_snapshot_preview_and_event_messages() {
|
||||
let second_payload: Value = serde_json::from_str(&second_frame).expect("second ws frame");
|
||||
assert_eq!(second_payload["message"]["type"], "preview");
|
||||
|
||||
let _ = send_http_request(
|
||||
let invalid = send_command_json(
|
||||
server.local_addr(),
|
||||
"POST",
|
||||
"/api/v1/command",
|
||||
Some(
|
||||
r#"{
|
||||
"request_id": "contract-event",
|
||||
"command": {
|
||||
"type": "set_blackout",
|
||||
"payload": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
),
|
||||
r#"{"command":{"type":"recall_creative_snapshot","payload":{"snapshot_id":"does_not_exist"}}}"#,
|
||||
);
|
||||
let invalid_body: Value = serde_json::from_str(&invalid.body).expect("invalid json");
|
||||
assert_eq!(invalid.status_code, 400);
|
||||
assert_eq!(invalid_body["error"]["code"], "unknown_creative_snapshot");
|
||||
|
||||
let mut saw_event = false;
|
||||
let mut saw_warning = false;
|
||||
for _ in 0..8 {
|
||||
let frame = read_websocket_text_frame(&mut stream);
|
||||
let payload: Value = serde_json::from_str(&frame).expect("ws event frame");
|
||||
let payload: Value = serde_json::from_str(&frame).expect("ws frame");
|
||||
if payload["message"]["type"] == "event" {
|
||||
saw_event = true;
|
||||
saw_warning = true;
|
||||
assert_eq!(payload["message"]["payload"]["kind"], "warning");
|
||||
assert_eq!(payload["message"]["payload"]["code"], "unknown_creative_snapshot");
|
||||
assert!(payload["message"]["payload"]["message"]
|
||||
.as_str()
|
||||
.expect("event message")
|
||||
.contains("blackout"));
|
||||
.contains("does_not_exist"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert!(saw_event, "expected websocket event after command");
|
||||
assert!(saw_warning, "expected warning event after failed command");
|
||||
|
||||
let _ = stream.shutdown(Shutdown::Both);
|
||||
server.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "longer load-oriented sequence for platform hardening"]
|
||||
fn load_sequence_keeps_state_preview_and_catalog_consistent() {
|
||||
let server = start_server();
|
||||
let patterns = ["solid_color", "gradient", "chase", "pulse", "noise"];
|
||||
let groups = [None, Some("top_panels"), Some("middle_panels"), Some("bottom_panels")];
|
||||
|
||||
for index in 0..80 {
|
||||
let pattern = patterns[index % patterns.len()];
|
||||
let group = groups[index % groups.len()];
|
||||
let brightness = ((index % 10) as f32) / 10.0;
|
||||
let speed = 0.5 + (index % 6) as f32 * 0.25;
|
||||
|
||||
let _ = send_command_json(
|
||||
server.local_addr(),
|
||||
&format!(
|
||||
r#"{{"command":{{"type":"select_pattern","payload":{{"pattern_id":"{pattern}"}}}}}}"#
|
||||
),
|
||||
);
|
||||
let _ = send_command_json(
|
||||
server.local_addr(),
|
||||
&format!(
|
||||
r#"{{"command":{{"type":"set_master_brightness","payload":{{"value":{brightness}}}}}}}"#
|
||||
),
|
||||
);
|
||||
let _ = send_command_json(
|
||||
server.local_addr(),
|
||||
&format!(
|
||||
r#"{{"command":{{"type":"set_scene_parameter","payload":{{"key":"speed","value":{{"kind":"scalar","value":{speed}}}}}}}}}"#
|
||||
),
|
||||
);
|
||||
let group_json = match group {
|
||||
Some(group_id) => format!(r#""{group_id}""#),
|
||||
None => "null".to_string(),
|
||||
};
|
||||
let _ = send_command_json(
|
||||
server.local_addr(),
|
||||
&format!(
|
||||
r#"{{"command":{{"type":"select_group","payload":{{"group_id":{group_json}}}}}}}"#
|
||||
),
|
||||
);
|
||||
|
||||
let state = send_http_request(server.local_addr(), "GET", "/api/v1/state", None);
|
||||
let preview = send_http_request(server.local_addr(), "GET", "/api/v1/preview", None);
|
||||
let catalog = send_http_request(server.local_addr(), "GET", "/api/v1/catalog", None);
|
||||
|
||||
let state_body: Value = serde_json::from_str(&state.body).expect("state json");
|
||||
let preview_body: Value = serde_json::from_str(&preview.body).expect("preview json");
|
||||
let catalog_body: Value = serde_json::from_str(&catalog.body).expect("catalog json");
|
||||
|
||||
assert_eq!(state_body["state"]["panels"].as_array().map(Vec::len), Some(18));
|
||||
assert_eq!(preview_body["preview"]["panels"].as_array().map(Vec::len), Some(18));
|
||||
assert!(catalog_body["patterns"].as_array().map(Vec::len).unwrap_or_default() >= 5);
|
||||
}
|
||||
|
||||
server.shutdown();
|
||||
}
|
||||
|
||||
fn send_command_json(addr: SocketAddr, body: &str) -> HttpResponse {
|
||||
send_http_request(addr, "POST", "/api/v1/command", Some(body))
|
||||
}
|
||||
|
||||
fn send_http_request(addr: SocketAddr, method: &str, path: &str, body: Option<&str>) -> HttpResponse {
|
||||
let body = body.unwrap_or("");
|
||||
let request = format!(
|
||||
@@ -293,3 +421,11 @@ fn read_websocket_text_frame(stream: &mut TcpStream) -> String {
|
||||
stream.read_exact(&mut payload).expect("frame payload");
|
||||
String::from_utf8(payload).expect("frame utf8")
|
||||
}
|
||||
|
||||
fn unique_runtime_state_path(label: &str) -> PathBuf {
|
||||
let millis = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("system time")
|
||||
.as_millis();
|
||||
std::env::temp_dir().join(format!("infinity_vis_{label}_{millis}.json"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user