quantik_core/bench/
bundle.rs1use serde_json::{json, Map, Value};
4use std::path::Path;
5use std::process::Command;
6
7use super::contracts::{CONTRACT_VERSION, GAME_RESULT_SCHEMA, OBSERVATION_SCHEMA};
8
9pub const SCHEMA_VERSION: u64 = 1;
10
11fn git_sha() -> String {
12 Command::new("git")
13 .args(["rev-parse", "HEAD"])
14 .output()
15 .ok()
16 .filter(|out| out.status.success())
17 .map(|out| String::from_utf8_lossy(&out.stdout).trim().to_string())
18 .unwrap_or_else(|| "unknown".into())
19}
20
21fn rustc_version() -> String {
27 Command::new("rustc")
28 .arg("--version")
29 .output()
30 .ok()
31 .filter(|out| out.status.success())
32 .and_then(|out| {
33 String::from_utf8_lossy(&out.stdout)
34 .split_whitespace()
35 .nth(1)
36 .map(str::to_string)
37 })
38 .unwrap_or_else(|| "unknown".into())
39}
40
41pub fn collect_environment() -> Value {
45 json!({
46 "quantik_core_version": env!("CARGO_PKG_VERSION"),
47 "git_sha": git_sha(),
48 "rust_version": rustc_version(),
49 "platform": format!("{}-{}", std::env::consts::OS, std::env::consts::ARCH),
50 "processor": std::env::consts::ARCH,
51 "cpu_count": std::thread::available_parallelism().map(|n| n.get()).unwrap_or(0),
52 "total_memory_bytes": Value::Null,
53 })
54}
55
56pub fn make_bundle(
58 config: Value,
59 dataset_payload: &Value,
60 observations: Vec<Value>,
61 head_to_head: Value,
62 aggregates: Value,
63) -> Value {
64 let positions = dataset_payload["positions"]
65 .as_array()
66 .cloned()
67 .unwrap_or_default();
68 let mut phases: Map<String, Value> = Map::new();
69 for position in &positions {
70 let phase = position["phase"].as_str().unwrap_or_default().to_string();
71 let count = phases.get(&phase).and_then(Value::as_u64).unwrap_or(0);
72 phases.insert(phase, json!(count + 1));
73 }
74
75 let started_at = chrono::Local::now()
77 .format("%Y-%m-%dT%H:%M:%S%z")
78 .to_string();
79
80 json!({
81 "contract_version": CONTRACT_VERSION,
82 "schema_version": SCHEMA_VERSION,
83 "started_at": started_at,
84 "artifact_contracts": {
85 "observations": OBSERVATION_SCHEMA,
86 "head_to_head": GAME_RESULT_SCHEMA,
87 },
88 "environment": collect_environment(),
89 "config": config,
90 "dataset": {
91 "checksum": dataset_payload.get("checksum").cloned().unwrap_or(Value::Null),
92 "generator": dataset_payload["generator"],
93 "seed": dataset_payload["seed"],
94 "schema_version": dataset_payload["schema_version"],
95 "positions": positions.len(),
96 "phases": phases,
97 },
98 "observations": observations,
99 "head_to_head": head_to_head,
100 "aggregates": aggregates,
101 })
102}
103
104pub fn save_bundle(bundle: &Value, path: &Path) -> Result<(), String> {
106 if let Some(parent) = path.parent() {
107 std::fs::create_dir_all(parent).map_err(|e| format!("mkdir {parent:?}: {e}"))?;
108 }
109 let text =
110 serde_json::to_string_pretty(bundle).map_err(|e| format!("serialize bundle: {e}"))?;
111 std::fs::write(path, text + "\n").map_err(|e| format!("write {path:?}: {e}"))
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn bundle_shape_and_save() {
120 let dataset = json!({
121 "checksum": "abc",
122 "generator": "g/v1",
123 "seed": 1,
124 "schema_version": 1,
125 "positions": [
126 {"phase": "opening"}, {"phase": "opening"}, {"phase": "endgame"},
127 ],
128 });
129 let bundle = make_bundle(
130 json!({"family": "fixed"}),
131 &dataset,
132 vec![json!({"engine": "random"})],
133 json!({"records": [], "aggregates": []}),
134 json!({"agreement": [], "cost": [], "stability": []}),
135 );
136 assert_eq!(bundle["schema_version"], json!(1));
137 assert_eq!(bundle["dataset"]["positions"], json!(3));
138 assert_eq!(bundle["dataset"]["phases"]["opening"], json!(2));
139 assert_eq!(bundle["config"]["family"], json!("fixed"));
140 assert_eq!(bundle["observations"].as_array().unwrap().len(), 1);
141 assert!(bundle["environment"]["quantik_core_version"].is_string());
142 let ts = bundle["started_at"].as_str().unwrap();
144 assert_eq!(ts.len(), 24, "{ts}");
145 assert_eq!(&ts[4..5], "-");
146 assert_eq!(&ts[10..11], "T");
147
148 let dir = std::env::temp_dir().join(format!("quantik-bundle-{}", std::process::id()));
149 let path = dir.join("nested").join("bundle.json");
150 save_bundle(&bundle, &path).unwrap();
151 let loaded: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
152 assert_eq!(loaded["dataset"]["checksum"], json!("abc"));
153 std::fs::remove_dir_all(&dir).ok();
154 }
155}