|  | 
| 1 |  | -use std::io; | 
| 2 |  | - | 
| 3 |  | -use crate::template::{ | 
| 4 |  | - all_days, | 
| 5 |  | - readme_benchmarks::{self, Timings}, | 
| 6 |  | - Day, ANSI_BOLD, ANSI_ITALIC, ANSI_RESET, | 
| 7 |  | -}; | 
|  | 1 | +use crate::template::{all_days, run_multi::run_multi}; | 
| 8 | 2 | 
 | 
| 9 | 3 | pub fn handle(is_release: bool, is_timed: bool) { | 
| 10 |  | - let mut timings: Vec<Timings> = vec![]; | 
| 11 |  | - | 
| 12 |  | - all_days().for_each(|day| { | 
| 13 |  | - if day > 1 { | 
| 14 |  | - println!(); | 
| 15 |  | - } | 
| 16 |  | - | 
| 17 |  | - println!("{ANSI_BOLD}Day {day}{ANSI_RESET}"); | 
| 18 |  | - println!("------"); | 
| 19 |  | - | 
| 20 |  | - let output = child_commands::run_solution(day, is_timed, is_release).unwrap(); | 
| 21 |  | - | 
| 22 |  | - if output.is_empty() { | 
| 23 |  | - println!("Not solved."); | 
| 24 |  | - } else { | 
| 25 |  | - let val = child_commands::parse_exec_time(&output, day); | 
| 26 |  | - timings.push(val); | 
| 27 |  | - } | 
| 28 |  | - }); | 
| 29 |  | - | 
| 30 |  | - if is_timed { | 
| 31 |  | - let total_millis = timings.iter().map(|x| x.total_nanos).sum::<f64>() / 1_000_000_f64; | 
| 32 |  | - | 
| 33 |  | - println!("\n{ANSI_BOLD}Total:{ANSI_RESET} {ANSI_ITALIC}{total_millis:.2}ms{ANSI_RESET}"); | 
| 34 |  | - | 
| 35 |  | - if is_release { | 
| 36 |  | - match readme_benchmarks::update(timings, total_millis) { | 
| 37 |  | - Ok(()) => println!("Successfully updated README with benchmarks."), | 
| 38 |  | - Err(_) => { | 
| 39 |  | - eprintln!("Failed to update readme with benchmarks."); | 
| 40 |  | - } | 
| 41 |  | - } | 
| 42 |  | - } | 
| 43 |  | - } | 
| 44 |  | -} | 
| 45 |  | - | 
| 46 |  | -#[derive(Debug)] | 
| 47 |  | -pub enum Error { | 
| 48 |  | - BrokenPipe, | 
| 49 |  | - Parser(String), | 
| 50 |  | - IO(io::Error), | 
| 51 |  | -} | 
| 52 |  | - | 
| 53 |  | -impl From<std::io::Error> for Error { | 
| 54 |  | - fn from(e: std::io::Error) -> Self { | 
| 55 |  | - Error::IO(e) | 
| 56 |  | - } | 
| 57 |  | -} | 
| 58 |  | - | 
| 59 |  | -#[must_use] | 
| 60 |  | -pub fn get_path_for_bin(day: Day) -> String { | 
| 61 |  | - format!("./src/bin/{day}.rs") | 
| 62 |  | -} | 
| 63 |  | - | 
| 64 |  | -/// All solutions live in isolated binaries. | 
| 65 |  | -/// This module encapsulates interaction with these binaries, both invoking them as well as parsing the timing output. | 
| 66 |  | -mod child_commands { | 
| 67 |  | - use super::{get_path_for_bin, Error}; | 
| 68 |  | - use crate::template::Day; | 
| 69 |  | - use std::{ | 
| 70 |  | - io::{BufRead, BufReader}, | 
| 71 |  | - path::Path, | 
| 72 |  | - process::{Command, Stdio}, | 
| 73 |  | - thread, | 
| 74 |  | - }; | 
| 75 |  | - | 
| 76 |  | - /// Run the solution bin for a given day | 
| 77 |  | - pub fn run_solution(day: Day, is_timed: bool, is_release: bool) -> Result<Vec<String>, Error> { | 
| 78 |  | - // skip command invocation for days that have not been scaffolded yet. | 
| 79 |  | - if !Path::new(&get_path_for_bin(day)).exists() { | 
| 80 |  | - return Ok(vec![]); | 
| 81 |  | - } | 
| 82 |  | - | 
| 83 |  | - let day_padded = day.to_string(); | 
| 84 |  | - let mut args = vec!["run", "--quiet", "--bin", &day_padded]; | 
| 85 |  | - | 
| 86 |  | - if is_release { | 
| 87 |  | - args.push("--release"); | 
| 88 |  | - } | 
| 89 |  | - | 
| 90 |  | - if is_timed { | 
| 91 |  | - // mirror `--time` flag to child invocations. | 
| 92 |  | - args.push("--"); | 
| 93 |  | - args.push("--time"); | 
| 94 |  | - } | 
| 95 |  | - | 
| 96 |  | - // spawn child command with piped stdout/stderr. | 
| 97 |  | - // forward output to stdout/stderr while grabbing stdout lines. | 
| 98 |  | - | 
| 99 |  | - let mut cmd = Command::new("cargo") | 
| 100 |  | - .args(&args) | 
| 101 |  | - .stdout(Stdio::piped()) | 
| 102 |  | - .stderr(Stdio::piped()) | 
| 103 |  | - .spawn()?; | 
| 104 |  | - | 
| 105 |  | - let stdout = BufReader::new(cmd.stdout.take().ok_or(super::Error::BrokenPipe)?); | 
| 106 |  | - let stderr = BufReader::new(cmd.stderr.take().ok_or(super::Error::BrokenPipe)?); | 
| 107 |  | - | 
| 108 |  | - let mut output = vec![]; | 
| 109 |  | - | 
| 110 |  | - let thread = thread::spawn(move || { | 
| 111 |  | - stderr.lines().for_each(|line| { | 
| 112 |  | - eprintln!("{}", line.unwrap()); | 
| 113 |  | - }); | 
| 114 |  | - }); | 
| 115 |  | - | 
| 116 |  | - for line in stdout.lines() { | 
| 117 |  | - let line = line.unwrap(); | 
| 118 |  | - println!("{line}"); | 
| 119 |  | - output.push(line); | 
| 120 |  | - } | 
| 121 |  | - | 
| 122 |  | - thread.join().unwrap(); | 
| 123 |  | - cmd.wait()?; | 
| 124 |  | - | 
| 125 |  | - Ok(output) | 
| 126 |  | - } | 
| 127 |  | - | 
| 128 |  | - pub fn parse_exec_time(output: &[String], day: Day) -> super::Timings { | 
| 129 |  | - let mut timings = super::Timings { | 
| 130 |  | - day, | 
| 131 |  | - part_1: None, | 
| 132 |  | - part_2: None, | 
| 133 |  | - total_nanos: 0_f64, | 
| 134 |  | - }; | 
| 135 |  | - | 
| 136 |  | - output | 
| 137 |  | - .iter() | 
| 138 |  | - .filter_map(|l| { | 
| 139 |  | - if !l.contains(" samples)") { | 
| 140 |  | - return None; | 
| 141 |  | - } | 
| 142 |  | - | 
| 143 |  | - let Some((timing_str, nanos)) = parse_time(l) else { | 
| 144 |  | - eprintln!("Could not parse timings from line: {l}"); | 
| 145 |  | - return None; | 
| 146 |  | - }; | 
| 147 |  | - | 
| 148 |  | - let part = l.split(':').next()?; | 
| 149 |  | - Some((part, timing_str, nanos)) | 
| 150 |  | - }) | 
| 151 |  | - .for_each(|(part, timing_str, nanos)| { | 
| 152 |  | - if part.contains("Part 1") { | 
| 153 |  | - timings.part_1 = Some(timing_str.into()); | 
| 154 |  | - } else if part.contains("Part 2") { | 
| 155 |  | - timings.part_2 = Some(timing_str.into()); | 
| 156 |  | - } | 
| 157 |  | - | 
| 158 |  | - timings.total_nanos += nanos; | 
| 159 |  | - }); | 
| 160 |  | - | 
| 161 |  | - timings | 
| 162 |  | - } | 
| 163 |  | - | 
| 164 |  | - fn parse_to_float(s: &str, postfix: &str) -> Option<f64> { | 
| 165 |  | - s.split(postfix).next()?.parse().ok() | 
| 166 |  | - } | 
| 167 |  | - | 
| 168 |  | - fn parse_time(line: &str) -> Option<(&str, f64)> { | 
| 169 |  | - // for possible time formats, see: https://github.com/rust-lang/rust/blob/1.64.0/library/core/src/time.rs#L1176-L1200 | 
| 170 |  | - let str_timing = line | 
| 171 |  | - .split(" samples)") | 
| 172 |  | - .next()? | 
| 173 |  | - .split('(') | 
| 174 |  | - .last()? | 
| 175 |  | - .split('@') | 
| 176 |  | - .next()? | 
| 177 |  | - .trim(); | 
| 178 |  | - | 
| 179 |  | - let parsed_timing = match str_timing { | 
| 180 |  | - s if s.contains("ns") => s.split("ns").next()?.parse::<f64>().ok(), | 
| 181 |  | - s if s.contains("μs") => parse_to_float(s, "μs").map(|x| x * 1000_f64), | 
| 182 |  | - s if s.contains("ms") => parse_to_float(s, "ms").map(|x| x * 1_000_000_f64), | 
| 183 |  | - s => parse_to_float(s, "s").map(|x| x * 1_000_000_000_f64), | 
| 184 |  | - }?; | 
| 185 |  | - | 
| 186 |  | - Some((str_timing, parsed_timing)) | 
| 187 |  | - } | 
| 188 |  | - | 
| 189 |  | - /// copied from: https://github.com/rust-lang/rust/blob/1.64.0/library/std/src/macros.rs#L328-L333 | 
| 190 |  | - #[cfg(feature = "test_lib")] | 
| 191 |  | - macro_rules! assert_approx_eq { | 
| 192 |  | - ($a:expr, $b:expr) => {{ | 
| 193 |  | - let (a, b) = (&$a, &$b); | 
| 194 |  | - assert!( | 
| 195 |  | - (*a - *b).abs() < 1.0e-6, | 
| 196 |  | - "{} is not approximately equal to {}", | 
| 197 |  | - *a, | 
| 198 |  | - *b | 
| 199 |  | - ); | 
| 200 |  | - }}; | 
| 201 |  | - } | 
| 202 |  | - | 
| 203 |  | - #[cfg(feature = "test_lib")] | 
| 204 |  | - mod tests { | 
| 205 |  | - use super::parse_exec_time; | 
| 206 |  | - | 
| 207 |  | - use crate::day; | 
| 208 |  | - | 
| 209 |  | - #[test] | 
| 210 |  | - fn test_well_formed() { | 
| 211 |  | - let res = parse_exec_time( | 
| 212 |  | - &[ | 
| 213 |  | - "Part 1: 0 (74.13ns @ 100000 samples)".into(), | 
| 214 |  | - "Part 2: 10 (74.13ms @ 99999 samples)".into(), | 
| 215 |  | - "".into(), | 
| 216 |  | - ], | 
| 217 |  | - day!(1), | 
| 218 |  | - ); | 
| 219 |  | - assert_approx_eq!(res.total_nanos, 74130074.13_f64); | 
| 220 |  | - assert_eq!(res.part_1.unwrap(), "74.13ns"); | 
| 221 |  | - assert_eq!(res.part_2.unwrap(), "74.13ms"); | 
| 222 |  | - } | 
| 223 |  | - | 
| 224 |  | - #[test] | 
| 225 |  | - fn test_patterns_in_input() { | 
| 226 |  | - let res = parse_exec_time( | 
| 227 |  | - &[ | 
| 228 |  | - "Part 1: @ @ @ ( ) ms (2s @ 5 samples)".into(), | 
| 229 |  | - "Part 2: 10s (100ms @ 1 samples)".into(), | 
| 230 |  | - "".into(), | 
| 231 |  | - ], | 
| 232 |  | - day!(1), | 
| 233 |  | - ); | 
| 234 |  | - assert_approx_eq!(res.total_nanos, 2100000000_f64); | 
| 235 |  | - assert_eq!(res.part_1.unwrap(), "2s"); | 
| 236 |  | - assert_eq!(res.part_2.unwrap(), "100ms"); | 
| 237 |  | - } | 
| 238 |  | - | 
| 239 |  | - #[test] | 
| 240 |  | - fn test_missing_parts() { | 
| 241 |  | - let res = parse_exec_time( | 
| 242 |  | - &[ | 
| 243 |  | - "Part 1: ✖ ".into(), | 
| 244 |  | - "Part 2: ✖ ".into(), | 
| 245 |  | - "".into(), | 
| 246 |  | - ], | 
| 247 |  | - day!(1), | 
| 248 |  | - ); | 
| 249 |  | - assert_approx_eq!(res.total_nanos, 0_f64); | 
| 250 |  | - assert_eq!(res.part_1.is_none(), true); | 
| 251 |  | - assert_eq!(res.part_2.is_none(), true); | 
| 252 |  | - } | 
| 253 |  | - } | 
|  | 4 | + run_multi(all_days().collect(), is_release, is_timed); | 
| 254 | 5 | } | 
0 commit comments