1use serde::{Deserialize, Serialize};
7use std::time::{SystemTime, UNIX_EPOCH};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct RunManifest {
12 pub version: String,
14 pub git_hash: Option<String>,
16 pub timestamp: String,
18 pub platform: PlatformInfo,
20 pub config: SearchConfigInfo,
22 pub results: Vec<MatchInfo>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct PlatformInfo {
29 pub os: String,
31 pub arch: String,
33 pub rust_version: String,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct SearchConfigInfo {
40 pub target: f64,
42 pub level: f32,
44 pub max_lhs_complexity: u32,
46 pub max_rhs_complexity: u32,
48 pub deterministic: bool,
50 pub parallel: bool,
52 pub max_error: f64,
54 pub max_matches: usize,
56 pub ranking_mode: String,
58 pub user_constants: Vec<UserConstantInfo>,
60 pub excluded_symbols: Vec<String>,
62 pub allowed_symbols: Option<Vec<String>>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct UserConstantInfo {
69 pub name: String,
71 pub value: f64,
73 pub description: String,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct MatchInfo {
80 pub lhs_postfix: String,
82 pub rhs_postfix: String,
84 pub lhs_infix: String,
86 pub rhs_infix: String,
88 pub error: f64,
90 pub is_exact: bool,
92 pub complexity: u32,
94 pub x_value: f64,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub stability: Option<f64>,
103}
104
105impl RunManifest {
106 pub fn new(config: SearchConfigInfo, results: Vec<MatchInfo>) -> Self {
108 let timestamp = SystemTime::now()
109 .duration_since(UNIX_EPOCH)
110 .map(|d| {
111 let secs = d.as_secs();
112 chrono_like_timestamp(secs)
114 })
115 .unwrap_or_else(|_| "unknown".to_string());
116
117 Self {
118 version: env!("CARGO_PKG_VERSION").to_string(),
119 git_hash: get_git_hash(),
120 timestamp,
121 platform: PlatformInfo::current(),
122 config,
123 results,
124 }
125 }
126
127 pub fn to_json(&self) -> Result<String, serde_json::Error> {
129 serde_json::to_string_pretty(self)
130 }
131
132 pub fn to_json_compact(&self) -> Result<String, serde_json::Error> {
134 serde_json::to_string(self)
135 }
136}
137
138impl PlatformInfo {
139 pub fn current() -> Self {
141 Self {
142 os: std::env::consts::OS.to_string(),
143 arch: std::env::consts::ARCH.to_string(),
144 rust_version: rustc_version().unwrap_or_else(|| "unknown".to_string()),
145 }
146 }
147}
148
149fn get_git_hash() -> Option<String> {
151 option_env!("GIT_HASH").map(|s| s.to_string()).or_else(|| {
153 #[cfg(debug_assertions)]
155 {
156 std::process::Command::new("git")
157 .args(["rev-parse", "--short", "HEAD"])
158 .output()
159 .ok()
160 .and_then(|o| String::from_utf8(o.stdout).ok())
161 .map(|s| s.trim().to_string())
162 }
163 #[cfg(not(debug_assertions))]
164 {
165 None
166 }
167 })
168}
169
170fn rustc_version() -> Option<String> {
172 #[cfg(debug_assertions)]
174 {
175 std::process::Command::new("rustc")
176 .arg("--version")
177 .output()
178 .ok()
179 .and_then(|o| String::from_utf8(o.stdout).ok())
180 .map(|s| s.trim().to_string())
181 }
182 #[cfg(not(debug_assertions))]
183 {
184 None
185 }
186}
187
188fn chrono_like_timestamp(secs: u64) -> String {
190 let days = secs / 86400;
192 let remaining = secs % 86400;
193 let hours = remaining / 3600;
194 let minutes = (remaining % 3600) / 60;
195 let seconds = remaining % 60;
196
197 let (year, month, day) = days_to_ymd(days);
200
201 format!(
202 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
203 year, month, day, hours, minutes, seconds
204 )
205}
206
207fn days_to_ymd(days: u64) -> (i32, u32, u32) {
209 let mut year = 1970_i32;
211 let mut remaining_days = days as i64;
212
213 loop {
214 let days_in_year = if is_leap_year(year) { 366 } else { 365 };
215 if remaining_days < days_in_year {
216 break;
217 }
218 remaining_days -= days_in_year;
219 year += 1;
220 }
221
222 let days_in_months = if is_leap_year(year) {
223 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
224 } else {
225 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
226 };
227
228 let mut month = 1_u32;
229 for &days_in_month in &days_in_months {
230 if remaining_days < days_in_month as i64 {
231 break;
232 }
233 remaining_days -= days_in_month as i64;
234 month += 1;
235 }
236
237 let day = (remaining_days + 1) as u32; (year, month, day)
239}
240
241fn is_leap_year(year: i32) -> bool {
242 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
243}
244
245#[cfg(test)]
246mod tests {
247 use super::MatchInfo;
248 use super::*;
249
250 #[test]
251 fn test_timestamp_format() {
252 let ts = chrono_like_timestamp(1705318245);
254 assert!(ts.starts_with("2024-01-"));
255 assert!(ts.ends_with("Z"));
256 }
257
258 #[test]
259 fn test_leap_year() {
260 assert!(is_leap_year(2024));
261 assert!(!is_leap_year(2023));
262 assert!(!is_leap_year(1900));
263 assert!(is_leap_year(2000));
264 }
265
266 #[test]
267 fn test_timestamp_leap_year_feb29() {
268 let ts = chrono_like_timestamp(951782400);
270 assert!(ts.starts_with("2000-02-29"), "got: {}", ts);
271 }
272
273 #[test]
274 fn test_timestamp_year_boundary() {
275 let ts = chrono_like_timestamp(946684799);
277 assert!(ts.starts_with("1999-12-31"), "got: {}", ts);
278 let ts2 = chrono_like_timestamp(946684800);
280 assert!(ts2.starts_with("2000-01-01"), "got: {}", ts2);
281 }
282
283 #[test]
284 fn test_leap_year_century_rules() {
285 assert!(!is_leap_year(1900));
287 assert!(!is_leap_year(2100));
289 assert!(is_leap_year(2000));
291 assert!(is_leap_year(2400));
293 }
294
295 #[test]
296 fn test_match_info_omits_optional_stability_when_absent() {
297 let info = MatchInfo {
298 lhs_postfix: "x".to_string(),
299 rhs_postfix: "1".to_string(),
300 lhs_infix: "x".to_string(),
301 rhs_infix: "1".to_string(),
302 error: 0.0,
303 is_exact: true,
304 complexity: 2,
305 x_value: 1.0,
306 stability: None,
307 };
308
309 let value = serde_json::to_value(info).expect("manifest match should serialize");
310 assert!(
311 value.get("stability").is_none(),
312 "stability should be omitted when unavailable"
313 );
314 }
315}