feat: make cargo time
incremental by default (#53)
Co-authored-by: Tristan Guichaoua <33934311+tguichaoua@users.noreply.github.com>
This commit is contained in:
parent
4c4232139a
commit
874f57b359
15 changed files with 757 additions and 300 deletions
|
@ -6,7 +6,7 @@ read = "run --quiet --release -- read"
|
||||||
|
|
||||||
solve = "run --quiet --release -- solve"
|
solve = "run --quiet --release -- solve"
|
||||||
all = "run --quiet --release -- all"
|
all = "run --quiet --release -- all"
|
||||||
time = "run --quiet --release -- all --release --time"
|
time = "run --quiet --release -- time"
|
||||||
|
|
||||||
[env]
|
[env]
|
||||||
AOC_YEAR = "2023"
|
AOC_YEAR = "2023"
|
||||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -24,3 +24,7 @@ data/puzzles/*
|
||||||
|
|
||||||
# Dhat
|
# Dhat
|
||||||
dhat-heap.json
|
dhat-heap.json
|
||||||
|
|
||||||
|
# Benchmarks
|
||||||
|
|
||||||
|
data/timings.json
|
||||||
|
|
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -24,6 +24,7 @@ dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"dhat",
|
"dhat",
|
||||||
"pico-args",
|
"pico-args",
|
||||||
|
"tinyjson",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -390,6 +391,12 @@ version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820"
|
checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinyjson"
|
||||||
|
version = "2.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ab95735ea2c8fd51154d01e39cf13912a78071c2d89abc49a7ef102a7dd725a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
|
|
|
@ -15,11 +15,12 @@ inherits = "release"
|
||||||
debug = 1
|
debug = 1
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
dhat-heap = ["dhat"]
|
||||||
today = ["chrono"]
|
today = ["chrono"]
|
||||||
test_lib = []
|
test_lib = []
|
||||||
dhat-heap = ["dhat"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { version = "0.4.31", optional = true }
|
chrono = { version = "0.4.31", optional = true }
|
||||||
pico-args = "0.5.0"
|
|
||||||
dhat = { version = "0.3.2", optional = true }
|
dhat = { version = "0.3.2", optional = true }
|
||||||
|
pico-args = "0.5.0"
|
||||||
|
tinyjson = "2"
|
||||||
|
|
10
README.md
10
README.md
|
@ -116,13 +116,15 @@ cargo all
|
||||||
# Total: 0.20ms
|
# Total: 0.20ms
|
||||||
```
|
```
|
||||||
|
|
||||||
This runs all solutions sequentially and prints output to the command-line. Same as for the `solve` command, the `--release` flag runs an optimized build.
|
This runs all solutions sequentially and prints output to the command-line. Same as for the `solve` command, the `--release` flag runs an optimized build and the `--time` flag outputs benchmarks.
|
||||||
|
|
||||||
#### Update readme benchmarks
|
### ➡️ Update readme benchmarks
|
||||||
|
|
||||||
The template can output a table with solution times to your readme. In order to generate a benchmarking table, run `cargo time`. If everything goes well, the command will output "_Successfully updated README with benchmarks._" after the execution finishes and the readme will be updated.
|
The template can write benchmark times to the README via the `cargo time` command.
|
||||||
|
|
||||||
Please note that these are not "scientific" benchmarks, understand them as a fun approximation. 😉 Timings, especially in the microseconds range, might change a bit between invocations.
|
By default, this command checks for missing benchmarks, runs those solutions, and then updates the table. If you want to (re-)time all solutions, run `cargo time --all`. If you want to (re-)time one specific solution, run `cargo time <day>`.
|
||||||
|
|
||||||
|
Please note that these are not _scientific_ benchmarks, understand them as a fun approximation. 😉 Timings, especially in the microseconds range, might change a bit between invocations.
|
||||||
|
|
||||||
### ➡️ Run all tests
|
### ➡️ Run all tests
|
||||||
|
|
||||||
|
|
15
src/main.rs
15
src/main.rs
|
@ -1,4 +1,4 @@
|
||||||
use advent_of_code::template::commands::{all, download, read, scaffold, solve};
|
use advent_of_code::template::commands::{all, download, read, scaffold, solve, time};
|
||||||
use args::{parse, AppArguments};
|
use args::{parse, AppArguments};
|
||||||
|
|
||||||
#[cfg(feature = "today")]
|
#[cfg(feature = "today")]
|
||||||
|
@ -32,6 +32,10 @@ mod args {
|
||||||
release: bool,
|
release: bool,
|
||||||
time: bool,
|
time: bool,
|
||||||
},
|
},
|
||||||
|
Time {
|
||||||
|
all: bool,
|
||||||
|
day: Option<Day>,
|
||||||
|
},
|
||||||
#[cfg(feature = "today")]
|
#[cfg(feature = "today")]
|
||||||
Today,
|
Today,
|
||||||
}
|
}
|
||||||
|
@ -44,6 +48,14 @@ mod args {
|
||||||
release: args.contains("--release"),
|
release: args.contains("--release"),
|
||||||
time: args.contains("--time"),
|
time: args.contains("--time"),
|
||||||
},
|
},
|
||||||
|
Some("time") => {
|
||||||
|
let all = args.contains("--all");
|
||||||
|
|
||||||
|
AppArguments::Time {
|
||||||
|
all,
|
||||||
|
day: args.opt_free_from_str()?,
|
||||||
|
}
|
||||||
|
}
|
||||||
Some("download") => AppArguments::Download {
|
Some("download") => AppArguments::Download {
|
||||||
day: args.free_from_str()?,
|
day: args.free_from_str()?,
|
||||||
},
|
},
|
||||||
|
@ -90,6 +102,7 @@ fn main() {
|
||||||
}
|
}
|
||||||
Ok(args) => match args {
|
Ok(args) => match args {
|
||||||
AppArguments::All { release, time } => all::handle(release, time),
|
AppArguments::All { release, time } => all::handle(release, time),
|
||||||
|
AppArguments::Time { day, all } => time::handle(day, all),
|
||||||
AppArguments::Download { day } => download::handle(day),
|
AppArguments::Download { day } => download::handle(day),
|
||||||
AppArguments::Read { day } => read::handle(day),
|
AppArguments::Read { day } => read::handle(day),
|
||||||
AppArguments::Scaffold { day, download } => {
|
AppArguments::Scaffold { day, download } => {
|
||||||
|
|
|
@ -11,7 +11,6 @@ pub enum AocCommandError {
|
||||||
CommandNotFound,
|
CommandNotFound,
|
||||||
CommandNotCallable,
|
CommandNotCallable,
|
||||||
BadExitStatus(Output),
|
BadExitStatus(Output),
|
||||||
IoError,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for AocCommandError {
|
impl Display for AocCommandError {
|
||||||
|
@ -22,7 +21,6 @@ impl Display for AocCommandError {
|
||||||
AocCommandError::BadExitStatus(_) => {
|
AocCommandError::BadExitStatus(_) => {
|
||||||
write!(f, "aoc-cli exited with a non-zero status.")
|
write!(f, "aoc-cli exited with a non-zero status.")
|
||||||
}
|
}
|
||||||
AocCommandError::IoError => write!(f, "could not write output files to file system."),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,254 +1,5 @@
|
||||||
use std::io;
|
use crate::template::{all_days, run_multi::run_multi};
|
||||||
|
|
||||||
use crate::template::{
|
|
||||||
all_days,
|
|
||||||
readme_benchmarks::{self, Timings},
|
|
||||||
Day, ANSI_BOLD, ANSI_ITALIC, ANSI_RESET,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn handle(is_release: bool, is_timed: bool) {
|
pub fn handle(is_release: bool, is_timed: bool) {
|
||||||
let mut timings: Vec<Timings> = vec![];
|
run_multi(all_days().collect(), is_release, is_timed);
|
||||||
|
|
||||||
all_days().for_each(|day| {
|
|
||||||
if day > 1 {
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("{ANSI_BOLD}Day {day}{ANSI_RESET}");
|
|
||||||
println!("------");
|
|
||||||
|
|
||||||
let output = child_commands::run_solution(day, is_timed, is_release).unwrap();
|
|
||||||
|
|
||||||
if output.is_empty() {
|
|
||||||
println!("Not solved.");
|
|
||||||
} else {
|
|
||||||
let val = child_commands::parse_exec_time(&output, day);
|
|
||||||
timings.push(val);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if is_timed {
|
|
||||||
let total_millis = timings.iter().map(|x| x.total_nanos).sum::<f64>() / 1_000_000_f64;
|
|
||||||
|
|
||||||
println!("\n{ANSI_BOLD}Total:{ANSI_RESET} {ANSI_ITALIC}{total_millis:.2}ms{ANSI_RESET}");
|
|
||||||
|
|
||||||
if is_release {
|
|
||||||
match readme_benchmarks::update(timings, total_millis) {
|
|
||||||
Ok(()) => println!("Successfully updated README with benchmarks."),
|
|
||||||
Err(_) => {
|
|
||||||
eprintln!("Failed to update readme with benchmarks.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
BrokenPipe,
|
|
||||||
Parser(String),
|
|
||||||
IO(io::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<std::io::Error> for Error {
|
|
||||||
fn from(e: std::io::Error) -> Self {
|
|
||||||
Error::IO(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn get_path_for_bin(day: Day) -> String {
|
|
||||||
format!("./src/bin/{day}.rs")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// All solutions live in isolated binaries.
|
|
||||||
/// This module encapsulates interaction with these binaries, both invoking them as well as parsing the timing output.
|
|
||||||
mod child_commands {
|
|
||||||
use super::{get_path_for_bin, Error};
|
|
||||||
use crate::template::Day;
|
|
||||||
use std::{
|
|
||||||
io::{BufRead, BufReader},
|
|
||||||
path::Path,
|
|
||||||
process::{Command, Stdio},
|
|
||||||
thread,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Run the solution bin for a given day
|
|
||||||
pub fn run_solution(day: Day, is_timed: bool, is_release: bool) -> Result<Vec<String>, Error> {
|
|
||||||
// skip command invocation for days that have not been scaffolded yet.
|
|
||||||
if !Path::new(&get_path_for_bin(day)).exists() {
|
|
||||||
return Ok(vec![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let day_padded = day.to_string();
|
|
||||||
let mut args = vec!["run", "--quiet", "--bin", &day_padded];
|
|
||||||
|
|
||||||
if is_release {
|
|
||||||
args.push("--release");
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_timed {
|
|
||||||
// mirror `--time` flag to child invocations.
|
|
||||||
args.push("--");
|
|
||||||
args.push("--time");
|
|
||||||
}
|
|
||||||
|
|
||||||
// spawn child command with piped stdout/stderr.
|
|
||||||
// forward output to stdout/stderr while grabbing stdout lines.
|
|
||||||
|
|
||||||
let mut cmd = Command::new("cargo")
|
|
||||||
.args(&args)
|
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.stderr(Stdio::piped())
|
|
||||||
.spawn()?;
|
|
||||||
|
|
||||||
let stdout = BufReader::new(cmd.stdout.take().ok_or(super::Error::BrokenPipe)?);
|
|
||||||
let stderr = BufReader::new(cmd.stderr.take().ok_or(super::Error::BrokenPipe)?);
|
|
||||||
|
|
||||||
let mut output = vec![];
|
|
||||||
|
|
||||||
let thread = thread::spawn(move || {
|
|
||||||
stderr.lines().for_each(|line| {
|
|
||||||
eprintln!("{}", line.unwrap());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
for line in stdout.lines() {
|
|
||||||
let line = line.unwrap();
|
|
||||||
println!("{line}");
|
|
||||||
output.push(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
thread.join().unwrap();
|
|
||||||
cmd.wait()?;
|
|
||||||
|
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_exec_time(output: &[String], day: Day) -> super::Timings {
|
|
||||||
let mut timings = super::Timings {
|
|
||||||
day,
|
|
||||||
part_1: None,
|
|
||||||
part_2: None,
|
|
||||||
total_nanos: 0_f64,
|
|
||||||
};
|
|
||||||
|
|
||||||
output
|
|
||||||
.iter()
|
|
||||||
.filter_map(|l| {
|
|
||||||
if !l.contains(" samples)") {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some((timing_str, nanos)) = parse_time(l) else {
|
|
||||||
eprintln!("Could not parse timings from line: {l}");
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
let part = l.split(':').next()?;
|
|
||||||
Some((part, timing_str, nanos))
|
|
||||||
})
|
|
||||||
.for_each(|(part, timing_str, nanos)| {
|
|
||||||
if part.contains("Part 1") {
|
|
||||||
timings.part_1 = Some(timing_str.into());
|
|
||||||
} else if part.contains("Part 2") {
|
|
||||||
timings.part_2 = Some(timing_str.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
timings.total_nanos += nanos;
|
|
||||||
});
|
|
||||||
|
|
||||||
timings
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_to_float(s: &str, postfix: &str) -> Option<f64> {
|
|
||||||
s.split(postfix).next()?.parse().ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_time(line: &str) -> Option<(&str, f64)> {
|
|
||||||
// for possible time formats, see: https://github.com/rust-lang/rust/blob/1.64.0/library/core/src/time.rs#L1176-L1200
|
|
||||||
let str_timing = line
|
|
||||||
.split(" samples)")
|
|
||||||
.next()?
|
|
||||||
.split('(')
|
|
||||||
.last()?
|
|
||||||
.split('@')
|
|
||||||
.next()?
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
let parsed_timing = match str_timing {
|
|
||||||
s if s.contains("ns") => s.split("ns").next()?.parse::<f64>().ok(),
|
|
||||||
s if s.contains("µs") => parse_to_float(s, "µs").map(|x| x * 1000_f64),
|
|
||||||
s if s.contains("ms") => parse_to_float(s, "ms").map(|x| x * 1_000_000_f64),
|
|
||||||
s => parse_to_float(s, "s").map(|x| x * 1_000_000_000_f64),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
Some((str_timing, parsed_timing))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// copied from: https://github.com/rust-lang/rust/blob/1.64.0/library/std/src/macros.rs#L328-L333
|
|
||||||
#[cfg(feature = "test_lib")]
|
|
||||||
macro_rules! assert_approx_eq {
|
|
||||||
($a:expr, $b:expr) => {{
|
|
||||||
let (a, b) = (&$a, &$b);
|
|
||||||
assert!(
|
|
||||||
(*a - *b).abs() < 1.0e-6,
|
|
||||||
"{} is not approximately equal to {}",
|
|
||||||
*a,
|
|
||||||
*b
|
|
||||||
);
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "test_lib")]
|
|
||||||
mod tests {
|
|
||||||
use super::parse_exec_time;
|
|
||||||
|
|
||||||
use crate::day;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_well_formed() {
|
|
||||||
let res = parse_exec_time(
|
|
||||||
&[
|
|
||||||
"Part 1: 0 (74.13ns @ 100000 samples)".into(),
|
|
||||||
"Part 2: 10 (74.13ms @ 99999 samples)".into(),
|
|
||||||
"".into(),
|
|
||||||
],
|
|
||||||
day!(1),
|
|
||||||
);
|
|
||||||
assert_approx_eq!(res.total_nanos, 74130074.13_f64);
|
|
||||||
assert_eq!(res.part_1.unwrap(), "74.13ns");
|
|
||||||
assert_eq!(res.part_2.unwrap(), "74.13ms");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_patterns_in_input() {
|
|
||||||
let res = parse_exec_time(
|
|
||||||
&[
|
|
||||||
"Part 1: @ @ @ ( ) ms (2s @ 5 samples)".into(),
|
|
||||||
"Part 2: 10s (100ms @ 1 samples)".into(),
|
|
||||||
"".into(),
|
|
||||||
],
|
|
||||||
day!(1),
|
|
||||||
);
|
|
||||||
assert_approx_eq!(res.total_nanos, 2100000000_f64);
|
|
||||||
assert_eq!(res.part_1.unwrap(), "2s");
|
|
||||||
assert_eq!(res.part_2.unwrap(), "100ms");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_missing_parts() {
|
|
||||||
let res = parse_exec_time(
|
|
||||||
&[
|
|
||||||
"Part 1: ✖ ".into(),
|
|
||||||
"Part 2: ✖ ".into(),
|
|
||||||
"".into(),
|
|
||||||
],
|
|
||||||
day!(1),
|
|
||||||
);
|
|
||||||
assert_approx_eq!(res.total_nanos, 0_f64);
|
|
||||||
assert_eq!(res.part_1.is_none(), true);
|
|
||||||
assert_eq!(res.part_2.is_none(), true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,3 +3,4 @@ pub mod download;
|
||||||
pub mod read;
|
pub mod read;
|
||||||
pub mod scaffold;
|
pub mod scaffold;
|
||||||
pub mod solve;
|
pub mod solve;
|
||||||
|
pub mod time;
|
||||||
|
|
35
src/template/commands/time.rs
Normal file
35
src/template/commands/time.rs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::template::run_multi::run_multi;
|
||||||
|
use crate::template::timings::Timings;
|
||||||
|
use crate::template::{all_days, readme_benchmarks, Day};
|
||||||
|
|
||||||
|
pub fn handle(day: Option<Day>, recreate_all: bool) {
|
||||||
|
let stored_timings = Timings::read_from_file();
|
||||||
|
|
||||||
|
let days_to_run = day.map(|day| HashSet::from([day])).unwrap_or_else(|| {
|
||||||
|
if recreate_all {
|
||||||
|
all_days().collect()
|
||||||
|
} else {
|
||||||
|
// when the `--all` flag is not set, filter out days that are fully benched.
|
||||||
|
all_days()
|
||||||
|
.filter(|day| !stored_timings.is_day_complete(day))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let timings = run_multi(days_to_run, true, true).unwrap();
|
||||||
|
|
||||||
|
let merged_timings = stored_timings.merge(&timings);
|
||||||
|
merged_timings.store_file().unwrap();
|
||||||
|
|
||||||
|
println!();
|
||||||
|
match readme_benchmarks::update(merged_timings) {
|
||||||
|
Ok(()) => {
|
||||||
|
println!("Stored updated benchmarks.")
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("Failed to store updated benchmarks.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,12 +2,15 @@ use std::{env, fs};
|
||||||
|
|
||||||
pub mod aoc_cli;
|
pub mod aoc_cli;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
mod day;
|
|
||||||
pub mod readme_benchmarks;
|
|
||||||
pub mod runner;
|
pub mod runner;
|
||||||
|
|
||||||
pub use day::*;
|
pub use day::*;
|
||||||
|
|
||||||
|
mod day;
|
||||||
|
mod readme_benchmarks;
|
||||||
|
mod run_multi;
|
||||||
|
mod timings;
|
||||||
|
|
||||||
pub const ANSI_ITALIC: &str = "\x1b[3m";
|
pub const ANSI_ITALIC: &str = "\x1b[3m";
|
||||||
pub const ANSI_BOLD: &str = "\x1b[1m";
|
pub const ANSI_BOLD: &str = "\x1b[1m";
|
||||||
pub const ANSI_RESET: &str = "\x1b[0m";
|
pub const ANSI_RESET: &str = "\x1b[0m";
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
/// The approach taken is similar to how `aoc-readme-stars` handles this.
|
/// The approach taken is similar to how `aoc-readme-stars` handles this.
|
||||||
use std::{fs, io};
|
use std::{fs, io};
|
||||||
|
|
||||||
|
use crate::template::timings::Timings;
|
||||||
use crate::template::Day;
|
use crate::template::Day;
|
||||||
|
|
||||||
static MARKER: &str = "<!--- benchmarking table --->";
|
static MARKER: &str = "<!--- benchmarking table --->";
|
||||||
|
@ -18,14 +19,6 @@ impl From<std::io::Error> for Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Timings {
|
|
||||||
pub day: Day,
|
|
||||||
pub part_1: Option<String>,
|
|
||||||
pub part_2: Option<String>,
|
|
||||||
pub total_nanos: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TablePosition {
|
pub struct TablePosition {
|
||||||
pos_start: usize,
|
pos_start: usize,
|
||||||
pos_end: usize,
|
pos_end: usize,
|
||||||
|
@ -58,7 +51,7 @@ fn locate_table(readme: &str) -> Result<TablePosition, Error> {
|
||||||
Ok(TablePosition { pos_start, pos_end })
|
Ok(TablePosition { pos_start, pos_end })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn construct_table(prefix: &str, timings: Vec<Timings>, total_millis: f64) -> String {
|
fn construct_table(prefix: &str, timings: Timings, total_millis: f64) -> String {
|
||||||
let header = format!("{prefix} Benchmarks");
|
let header = format!("{prefix} Benchmarks");
|
||||||
|
|
||||||
let mut lines: Vec<String> = vec![
|
let mut lines: Vec<String> = vec![
|
||||||
|
@ -69,7 +62,7 @@ fn construct_table(prefix: &str, timings: Vec<Timings>, total_millis: f64) -> St
|
||||||
"| :---: | :---: | :---: |".into(),
|
"| :---: | :---: | :---: |".into(),
|
||||||
];
|
];
|
||||||
|
|
||||||
for timing in timings {
|
for timing in timings.data {
|
||||||
let path = get_path_for_bin(timing.day);
|
let path = get_path_for_bin(timing.day);
|
||||||
lines.push(format!(
|
lines.push(format!(
|
||||||
"| [Day {}]({}) | `{}` | `{}` |",
|
"| [Day {}]({}) | `{}` | `{}` |",
|
||||||
|
@ -87,16 +80,17 @@ fn construct_table(prefix: &str, timings: Vec<Timings>, total_millis: f64) -> St
|
||||||
lines.join("\n")
|
lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_content(s: &mut String, timings: Vec<Timings>, total_millis: f64) -> Result<(), Error> {
|
fn update_content(s: &mut String, timings: Timings, total_millis: f64) -> Result<(), Error> {
|
||||||
let positions = locate_table(s)?;
|
let positions = locate_table(s)?;
|
||||||
let table = construct_table("##", timings, total_millis);
|
let table = construct_table("##", timings, total_millis);
|
||||||
s.replace_range(positions.pos_start..positions.pos_end, &table);
|
s.replace_range(positions.pos_start..positions.pos_end, &table);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(timings: Vec<Timings>, total_millis: f64) -> Result<(), Error> {
|
pub fn update(timings: Timings) -> Result<(), Error> {
|
||||||
let path = "README.md";
|
let path = "README.md";
|
||||||
let mut readme = String::from_utf8_lossy(&fs::read(path)?).to_string();
|
let mut readme = String::from_utf8_lossy(&fs::read(path)?).to_string();
|
||||||
|
let total_millis = timings.total_millis();
|
||||||
update_content(&mut readme, timings, total_millis)?;
|
update_content(&mut readme, timings, total_millis)?;
|
||||||
fs::write(path, &readme)?;
|
fs::write(path, &readme)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -104,30 +98,32 @@ pub fn update(timings: Vec<Timings>, total_millis: f64) -> Result<(), Error> {
|
||||||
|
|
||||||
#[cfg(feature = "test_lib")]
|
#[cfg(feature = "test_lib")]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{update_content, Timings, MARKER};
|
use super::{update_content, MARKER};
|
||||||
use crate::day;
|
use crate::{day, template::timings::Timing, template::timings::Timings};
|
||||||
|
|
||||||
fn get_mock_timings() -> Vec<Timings> {
|
fn get_mock_timings() -> Timings {
|
||||||
vec![
|
Timings {
|
||||||
Timings {
|
data: vec![
|
||||||
day: day!(1),
|
Timing {
|
||||||
part_1: Some("10ms".into()),
|
day: day!(1),
|
||||||
part_2: Some("20ms".into()),
|
part_1: Some("10ms".into()),
|
||||||
total_nanos: 3e+10,
|
part_2: Some("20ms".into()),
|
||||||
},
|
total_nanos: 3e+10,
|
||||||
Timings {
|
},
|
||||||
day: day!(2),
|
Timing {
|
||||||
part_1: Some("30ms".into()),
|
day: day!(2),
|
||||||
part_2: Some("40ms".into()),
|
part_1: Some("30ms".into()),
|
||||||
total_nanos: 7e+10,
|
part_2: Some("40ms".into()),
|
||||||
},
|
total_nanos: 7e+10,
|
||||||
Timings {
|
},
|
||||||
day: day!(4),
|
Timing {
|
||||||
part_1: Some("40ms".into()),
|
day: day!(4),
|
||||||
part_2: Some("50ms".into()),
|
part_1: Some("40ms".into()),
|
||||||
total_nanos: 9e+10,
|
part_2: Some("50ms".into()),
|
||||||
},
|
total_nanos: 9e+10,
|
||||||
]
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
255
src/template/run_multi.rs
Normal file
255
src/template/run_multi.rs
Normal file
|
@ -0,0 +1,255 @@
|
||||||
|
use std::{collections::HashSet, io};
|
||||||
|
|
||||||
|
use crate::template::{Day, ANSI_BOLD, ANSI_ITALIC, ANSI_RESET};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
all_days,
|
||||||
|
timings::{Timing, Timings},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn run_multi(days_to_run: HashSet<Day>, is_release: bool, is_timed: bool) -> Option<Timings> {
|
||||||
|
let mut timings: Vec<Timing> = Vec::with_capacity(days_to_run.len());
|
||||||
|
|
||||||
|
all_days().for_each(|day| {
|
||||||
|
if day > 1 {
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{ANSI_BOLD}Day {day}{ANSI_RESET}");
|
||||||
|
println!("------");
|
||||||
|
|
||||||
|
if !days_to_run.contains(&day) {
|
||||||
|
println!("Skipped.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = child_commands::run_solution(day, is_timed, is_release).unwrap();
|
||||||
|
|
||||||
|
if output.is_empty() {
|
||||||
|
println!("Not solved.");
|
||||||
|
} else {
|
||||||
|
let val = child_commands::parse_exec_time(&output, day);
|
||||||
|
timings.push(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if is_timed {
|
||||||
|
let timings = Timings { data: timings };
|
||||||
|
let total_millis = timings.total_millis();
|
||||||
|
println!(
|
||||||
|
"\n{ANSI_BOLD}Total (Run):{ANSI_RESET} {ANSI_ITALIC}{total_millis:.2}ms{ANSI_RESET}"
|
||||||
|
);
|
||||||
|
Some(timings)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
BrokenPipe,
|
||||||
|
IO(io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for Error {
|
||||||
|
fn from(e: std::io::Error) -> Self {
|
||||||
|
Error::IO(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn get_path_for_bin(day: Day) -> String {
|
||||||
|
format!("./src/bin/{day}.rs")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All solutions live in isolated binaries.
|
||||||
|
/// This module encapsulates interaction with these binaries, both invoking them as well as parsing the timing output.
|
||||||
|
pub mod child_commands {
|
||||||
|
use super::{get_path_for_bin, Error};
|
||||||
|
use crate::template::Day;
|
||||||
|
use std::{
|
||||||
|
io::{BufRead, BufReader},
|
||||||
|
path::Path,
|
||||||
|
process::{Command, Stdio},
|
||||||
|
thread,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Run the solution bin for a given day
|
||||||
|
pub fn run_solution(day: Day, is_timed: bool, is_release: bool) -> Result<Vec<String>, Error> {
|
||||||
|
// skip command invocation for days that have not been scaffolded yet.
|
||||||
|
if !Path::new(&get_path_for_bin(day)).exists() {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let day_padded = day.to_string();
|
||||||
|
let mut args = vec!["run", "--quiet", "--bin", &day_padded];
|
||||||
|
|
||||||
|
if is_release {
|
||||||
|
args.push("--release");
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_timed {
|
||||||
|
// mirror `--time` flag to child invocations.
|
||||||
|
args.push("--");
|
||||||
|
args.push("--time");
|
||||||
|
}
|
||||||
|
|
||||||
|
// spawn child command with piped stdout/stderr.
|
||||||
|
// forward output to stdout/stderr while grabbing stdout lines.
|
||||||
|
|
||||||
|
let mut cmd = Command::new("cargo")
|
||||||
|
.args(&args)
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
let stdout = BufReader::new(cmd.stdout.take().ok_or(super::Error::BrokenPipe)?);
|
||||||
|
let stderr = BufReader::new(cmd.stderr.take().ok_or(super::Error::BrokenPipe)?);
|
||||||
|
|
||||||
|
let mut output = vec![];
|
||||||
|
|
||||||
|
let thread = thread::spawn(move || {
|
||||||
|
stderr.lines().for_each(|line| {
|
||||||
|
eprintln!("{}", line.unwrap());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
for line in stdout.lines() {
|
||||||
|
let line = line.unwrap();
|
||||||
|
println!("{line}");
|
||||||
|
output.push(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
thread.join().unwrap();
|
||||||
|
cmd.wait()?;
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_exec_time(output: &[String], day: Day) -> super::Timing {
|
||||||
|
let mut timings = super::Timing {
|
||||||
|
day,
|
||||||
|
part_1: None,
|
||||||
|
part_2: None,
|
||||||
|
total_nanos: 0_f64,
|
||||||
|
};
|
||||||
|
|
||||||
|
output
|
||||||
|
.iter()
|
||||||
|
.filter_map(|l| {
|
||||||
|
if !l.contains(" samples)") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some((timing_str, nanos)) = parse_time(l) else {
|
||||||
|
eprintln!("Could not parse timings from line: {l}");
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let part = l.split(':').next()?;
|
||||||
|
Some((part, timing_str, nanos))
|
||||||
|
})
|
||||||
|
.for_each(|(part, timing_str, nanos)| {
|
||||||
|
if part.contains("Part 1") {
|
||||||
|
timings.part_1 = Some(timing_str.into());
|
||||||
|
} else if part.contains("Part 2") {
|
||||||
|
timings.part_2 = Some(timing_str.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
timings.total_nanos += nanos;
|
||||||
|
});
|
||||||
|
|
||||||
|
timings
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_to_float(s: &str, postfix: &str) -> Option<f64> {
|
||||||
|
s.split(postfix).next()?.parse().ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_time(line: &str) -> Option<(&str, f64)> {
|
||||||
|
// for possible time formats, see: https://github.com/rust-lang/rust/blob/1.64.0/library/core/src/time.rs#L1176-L1200
|
||||||
|
let str_timing = line
|
||||||
|
.split(" samples)")
|
||||||
|
.next()?
|
||||||
|
.split('(')
|
||||||
|
.last()?
|
||||||
|
.split('@')
|
||||||
|
.next()?
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
let parsed_timing = match str_timing {
|
||||||
|
s if s.contains("ns") => s.split("ns").next()?.parse::<f64>().ok(),
|
||||||
|
s if s.contains("µs") => parse_to_float(s, "µs").map(|x| x * 1000_f64),
|
||||||
|
s if s.contains("ms") => parse_to_float(s, "ms").map(|x| x * 1_000_000_f64),
|
||||||
|
s => parse_to_float(s, "s").map(|x| x * 1_000_000_000_f64),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
Some((str_timing, parsed_timing))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// copied from: https://github.com/rust-lang/rust/blob/1.64.0/library/std/src/macros.rs#L328-L333
|
||||||
|
#[cfg(feature = "test_lib")]
|
||||||
|
macro_rules! assert_approx_eq {
|
||||||
|
($a:expr, $b:expr) => {{
|
||||||
|
let (a, b) = (&$a, &$b);
|
||||||
|
assert!(
|
||||||
|
(*a - *b).abs() < 1.0e-6,
|
||||||
|
"{} is not approximately equal to {}",
|
||||||
|
*a,
|
||||||
|
*b
|
||||||
|
);
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "test_lib")]
|
||||||
|
mod tests {
|
||||||
|
use super::parse_exec_time;
|
||||||
|
|
||||||
|
use crate::day;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_execution_times() {
|
||||||
|
let res = parse_exec_time(
|
||||||
|
&[
|
||||||
|
"Part 1: 0 (74.13ns @ 100000 samples)".into(),
|
||||||
|
"Part 2: 10 (74.13ms @ 99999 samples)".into(),
|
||||||
|
"".into(),
|
||||||
|
],
|
||||||
|
day!(1),
|
||||||
|
);
|
||||||
|
assert_approx_eq!(res.total_nanos, 74130074.13_f64);
|
||||||
|
assert_eq!(res.part_1.unwrap(), "74.13ns");
|
||||||
|
assert_eq!(res.part_2.unwrap(), "74.13ms");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_with_patterns_in_input() {
|
||||||
|
let res = parse_exec_time(
|
||||||
|
&[
|
||||||
|
"Part 1: @ @ @ ( ) ms (2s @ 5 samples)".into(),
|
||||||
|
"Part 2: 10s (100ms @ 1 samples)".into(),
|
||||||
|
"".into(),
|
||||||
|
],
|
||||||
|
day!(1),
|
||||||
|
);
|
||||||
|
assert_approx_eq!(res.total_nanos, 2100000000_f64);
|
||||||
|
assert_eq!(res.part_1.unwrap(), "2s");
|
||||||
|
assert_eq!(res.part_2.unwrap(), "100ms");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_missing_parts() {
|
||||||
|
let res = parse_exec_time(
|
||||||
|
&[
|
||||||
|
"Part 1: ✖ ".into(),
|
||||||
|
"Part 2: ✖ ".into(),
|
||||||
|
"".into(),
|
||||||
|
],
|
||||||
|
day!(1),
|
||||||
|
);
|
||||||
|
assert_approx_eq!(res.total_nanos, 0_f64);
|
||||||
|
assert_eq!(res.part_1.is_none(), true);
|
||||||
|
assert_eq!(res.part_2.is_none(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
/// Encapsulates code that interacts with solution functions.
|
/// Encapsulates code that interacts with solution functions.
|
||||||
use crate::template::{aoc_cli, Day, ANSI_ITALIC, ANSI_RESET};
|
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::hint::black_box;
|
use std::hint::black_box;
|
||||||
use std::io::{stdout, Write};
|
use std::io::{stdout, Write};
|
||||||
|
@ -7,7 +6,8 @@ use std::process::Output;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use std::{cmp, env, process};
|
use std::{cmp, env, process};
|
||||||
|
|
||||||
use super::ANSI_BOLD;
|
use crate::template::ANSI_BOLD;
|
||||||
|
use crate::template::{aoc_cli, Day, ANSI_ITALIC, ANSI_RESET};
|
||||||
|
|
||||||
pub fn run_part<I: Clone, T: Display>(func: impl Fn(I) -> Option<T>, input: I, day: Day, part: u8) {
|
pub fn run_part<I: Clone, T: Display>(func: impl Fn(I) -> Option<T>, input: I, day: Day, part: u8) {
|
||||||
let part_str = format!("Part {part}");
|
let part_str = format!("Part {part}");
|
||||||
|
|
391
src/template/timings.rs
Normal file
391
src/template/timings.rs
Normal file
|
@ -0,0 +1,391 @@
|
||||||
|
use std::{collections::HashMap, fs, io::Error, str::FromStr};
|
||||||
|
use tinyjson::JsonValue;
|
||||||
|
|
||||||
|
use crate::template::Day;
|
||||||
|
|
||||||
|
static TIMINGS_FILE_PATH: &str = "./data/timings.json";
|
||||||
|
|
||||||
|
/// Represents benchmark times for a single day.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Timing {
|
||||||
|
pub day: Day,
|
||||||
|
pub part_1: Option<String>,
|
||||||
|
pub part_2: Option<String>,
|
||||||
|
pub total_nanos: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents benchmark times for a set of days.
|
||||||
|
/// Can be serialized from / to JSON.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct Timings {
|
||||||
|
pub data: Vec<Timing>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Timings {
|
||||||
|
/// Dehydrate timings to a JSON file.
|
||||||
|
pub fn store_file(&self) -> Result<(), Error> {
|
||||||
|
let json = JsonValue::from(self.clone());
|
||||||
|
let mut file = fs::File::create(TIMINGS_FILE_PATH)?;
|
||||||
|
json.format_to(&mut file)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rehydrate timings from a JSON file. If not present, returns empty timings.
|
||||||
|
pub fn read_from_file() -> Self {
|
||||||
|
let s = fs::read_to_string(TIMINGS_FILE_PATH)
|
||||||
|
.map_err(|x| x.to_string())
|
||||||
|
.and_then(Timings::try_from);
|
||||||
|
|
||||||
|
match s {
|
||||||
|
Ok(timings) => timings,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{}", e);
|
||||||
|
Timings::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge two sets of timings, overwriting `self` with `other` if present.
|
||||||
|
pub fn merge(&self, new: &Self) -> Self {
|
||||||
|
let mut data: Vec<Timing> = vec![];
|
||||||
|
|
||||||
|
for timing in &new.data {
|
||||||
|
data.push(timing.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
for timing in &self.data {
|
||||||
|
if !data.iter().any(|t| t.day == timing.day) {
|
||||||
|
data.push(timing.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.sort_unstable_by(|a, b| a.day.cmp(&b.day));
|
||||||
|
Timings { data }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sum up total duration of timings as millis.
|
||||||
|
pub fn total_millis(&self) -> f64 {
|
||||||
|
self.data.iter().map(|x| x.total_nanos).sum::<f64>() / 1_000_000_f64
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_day_complete(&self, day: &Day) -> bool {
|
||||||
|
self.data
|
||||||
|
.iter()
|
||||||
|
.any(|t| &t.day == day && t.part_1.is_some() && t.part_2.is_some())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
impl From<Timings> for JsonValue {
|
||||||
|
fn from(value: Timings) -> Self {
|
||||||
|
let mut map: HashMap<String, JsonValue> = HashMap::new();
|
||||||
|
|
||||||
|
map.insert(
|
||||||
|
"data".into(),
|
||||||
|
JsonValue::Array(value.data.iter().map(JsonValue::from).collect()),
|
||||||
|
);
|
||||||
|
|
||||||
|
JsonValue::Object(map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<String> for Timings {
|
||||||
|
type Error = String;
|
||||||
|
|
||||||
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||||
|
let json = JsonValue::from_str(&value).or(Err("not valid JSON file."))?;
|
||||||
|
|
||||||
|
let json_data = json
|
||||||
|
.get::<HashMap<String, JsonValue>>()
|
||||||
|
.ok_or("expected JSON document to be an object.")?
|
||||||
|
.get("data")
|
||||||
|
.ok_or("expected JSON document to have key `data`.")?
|
||||||
|
.get::<Vec<JsonValue>>()
|
||||||
|
.ok_or("expected `json.data` to be an array.")?;
|
||||||
|
|
||||||
|
Ok(Timings {
|
||||||
|
data: json_data
|
||||||
|
.iter()
|
||||||
|
.map(Timing::try_from)
|
||||||
|
.collect::<Result<_, _>>()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
impl From<&Timing> for JsonValue {
|
||||||
|
fn from(value: &Timing) -> Self {
|
||||||
|
let mut map: HashMap<String, JsonValue> = HashMap::new();
|
||||||
|
|
||||||
|
map.insert("day".into(), JsonValue::String(value.day.to_string()));
|
||||||
|
map.insert("total_nanos".into(), JsonValue::Number(value.total_nanos));
|
||||||
|
|
||||||
|
let part_1 = value.part_1.clone().map(JsonValue::String);
|
||||||
|
let part_2 = value.part_2.clone().map(JsonValue::String);
|
||||||
|
|
||||||
|
map.insert(
|
||||||
|
"part_1".into(),
|
||||||
|
match part_1 {
|
||||||
|
Some(x) => x,
|
||||||
|
None => JsonValue::Null,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
map.insert(
|
||||||
|
"part_2".into(),
|
||||||
|
match part_2 {
|
||||||
|
Some(x) => x,
|
||||||
|
None => JsonValue::Null,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
JsonValue::Object(map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&JsonValue> for Timing {
|
||||||
|
type Error = String;
|
||||||
|
|
||||||
|
fn try_from(value: &JsonValue) -> Result<Self, Self::Error> {
|
||||||
|
let json = value
|
||||||
|
.get::<HashMap<String, JsonValue>>()
|
||||||
|
.ok_or("Expected timing to be a JSON object.")?;
|
||||||
|
|
||||||
|
let day = json
|
||||||
|
.get("day")
|
||||||
|
.and_then(|v| v.get::<String>())
|
||||||
|
.and_then(|day| Day::from_str(day).ok())
|
||||||
|
.ok_or("Expected timing.day to be a Day struct.")?;
|
||||||
|
|
||||||
|
let part_1 = json
|
||||||
|
.get("part_1")
|
||||||
|
.map(|v| if v.is_null() { None } else { v.get::<String>() })
|
||||||
|
.ok_or("Expected timing.part_1 to be null or string.")?;
|
||||||
|
|
||||||
|
let part_2 = json
|
||||||
|
.get("part_2")
|
||||||
|
.map(|v| if v.is_null() { None } else { v.get::<String>() })
|
||||||
|
.ok_or("Expected timing.part_2 to be null or string.")?;
|
||||||
|
|
||||||
|
let total_nanos = json
|
||||||
|
.get("total_nanos")
|
||||||
|
.and_then(|v| v.get::<f64>().copied())
|
||||||
|
.ok_or("Expected timing.total_nanos to be a number.")?;
|
||||||
|
|
||||||
|
Ok(Timing {
|
||||||
|
day,
|
||||||
|
part_1: part_1.cloned(),
|
||||||
|
part_2: part_2.cloned(),
|
||||||
|
total_nanos,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
#[cfg(feature = "test_lib")]
|
||||||
|
mod tests {
|
||||||
|
use crate::day;
|
||||||
|
|
||||||
|
use super::{Timing, Timings};
|
||||||
|
|
||||||
|
fn get_mock_timings() -> Timings {
|
||||||
|
Timings {
|
||||||
|
data: vec![
|
||||||
|
Timing {
|
||||||
|
day: day!(1),
|
||||||
|
part_1: Some("10ms".into()),
|
||||||
|
part_2: Some("20ms".into()),
|
||||||
|
total_nanos: 3e+10,
|
||||||
|
},
|
||||||
|
Timing {
|
||||||
|
day: day!(2),
|
||||||
|
part_1: Some("30ms".into()),
|
||||||
|
part_2: Some("40ms".into()),
|
||||||
|
total_nanos: 7e+10,
|
||||||
|
},
|
||||||
|
Timing {
|
||||||
|
day: day!(4),
|
||||||
|
part_1: Some("40ms".into()),
|
||||||
|
part_2: None,
|
||||||
|
total_nanos: 4e+10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod deserialization {
|
||||||
|
use crate::{day, template::timings::Timings};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handles_json_timings() {
|
||||||
|
let json = r#"{ "data": [{ "day": "01", "part_1": "1ms", "part_2": null, "total_nanos": 1000000000 }] }"#.to_string();
|
||||||
|
let timings = Timings::try_from(json).unwrap();
|
||||||
|
assert_eq!(timings.data.len(), 1);
|
||||||
|
let timing = timings.data.first().unwrap();
|
||||||
|
assert_eq!(timing.day, day!(1));
|
||||||
|
assert_eq!(timing.part_1, Some("1ms".to_string()));
|
||||||
|
assert_eq!(timing.part_2, None);
|
||||||
|
assert_eq!(timing.total_nanos, 1_000_000_000_f64);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handles_empty_timings() {
|
||||||
|
let json = r#"{ "data": [] }"#.to_string();
|
||||||
|
let timings = Timings::try_from(json).unwrap();
|
||||||
|
assert_eq!(timings.data.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn panics_for_invalid_json() {
|
||||||
|
let json = r#"{}"#.to_string();
|
||||||
|
Timings::try_from(json).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn panics_for_malformed_timings() {
|
||||||
|
let json = r#"{ "data": [{ "day": "01" }, { "day": "26" }, { "day": "02", "part_2": null, "total_nanos": 0 }] }"#.to_string();
|
||||||
|
Timings::try_from(json).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod serialization {
|
||||||
|
use super::get_mock_timings;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use tinyjson::JsonValue;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serializes_timings() {
|
||||||
|
let timings = get_mock_timings();
|
||||||
|
let value = JsonValue::try_from(timings).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
value
|
||||||
|
.get::<HashMap<String, JsonValue>>()
|
||||||
|
.unwrap()
|
||||||
|
.get("data")
|
||||||
|
.unwrap()
|
||||||
|
.get::<Vec<JsonValue>>()
|
||||||
|
.unwrap()
|
||||||
|
.len(),
|
||||||
|
3
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod is_day_complete {
|
||||||
|
use crate::{
|
||||||
|
day,
|
||||||
|
template::timings::{Timing, Timings},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handles_completed_days() {
|
||||||
|
let timings = Timings {
|
||||||
|
data: vec![Timing {
|
||||||
|
day: day!(1),
|
||||||
|
part_1: Some("1ms".into()),
|
||||||
|
part_2: Some("2ms".into()),
|
||||||
|
total_nanos: 3_000_000_000_f64,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(timings.is_day_complete(&day!(1)), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handles_partial_days() {
|
||||||
|
let timings = Timings {
|
||||||
|
data: vec![Timing {
|
||||||
|
day: day!(1),
|
||||||
|
part_1: Some("1ms".into()),
|
||||||
|
part_2: None,
|
||||||
|
total_nanos: 1_000_000_000_f64,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(timings.is_day_complete(&day!(1)), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handles_uncompleted_days() {
|
||||||
|
let timings = Timings {
|
||||||
|
data: vec![Timing {
|
||||||
|
day: day!(1),
|
||||||
|
part_1: None,
|
||||||
|
part_2: None,
|
||||||
|
total_nanos: 0.0,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(timings.is_day_complete(&day!(1)), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod merge {
|
||||||
|
use crate::{
|
||||||
|
day,
|
||||||
|
template::timings::{Timing, Timings},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::get_mock_timings;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handles_disjunct_timings() {
|
||||||
|
let timings = get_mock_timings();
|
||||||
|
let other = Timings {
|
||||||
|
data: vec![Timing {
|
||||||
|
day: day!(3),
|
||||||
|
part_1: None,
|
||||||
|
part_2: None,
|
||||||
|
total_nanos: 0_f64,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
let merged = timings.merge(&other);
|
||||||
|
assert_eq!(merged.data.len(), 4);
|
||||||
|
assert_eq!(merged.data[0].day, day!(1));
|
||||||
|
assert_eq!(merged.data[1].day, day!(2));
|
||||||
|
assert_eq!(merged.data[2].day, day!(3));
|
||||||
|
assert_eq!(merged.data[3].day, day!(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handles_overlapping_timings() {
|
||||||
|
let timings = get_mock_timings();
|
||||||
|
|
||||||
|
let other = Timings {
|
||||||
|
data: vec![Timing {
|
||||||
|
day: day!(2),
|
||||||
|
part_1: None,
|
||||||
|
part_2: None,
|
||||||
|
total_nanos: 0_f64,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
let merged = timings.merge(&other);
|
||||||
|
|
||||||
|
assert_eq!(merged.data.len(), 3);
|
||||||
|
assert_eq!(merged.data[0].day, day!(1));
|
||||||
|
assert_eq!(merged.data[1].day, day!(2));
|
||||||
|
assert_eq!(merged.data[1].total_nanos, 0_f64);
|
||||||
|
assert_eq!(merged.data[2].day, day!(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handles_empty_timings() {
|
||||||
|
let timings = Timings::default();
|
||||||
|
let other = get_mock_timings();
|
||||||
|
let merged = timings.merge(&other);
|
||||||
|
assert_eq!(merged.data.len(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handles_empty_other_timings() {
|
||||||
|
let timings = get_mock_timings();
|
||||||
|
let other = Timings::default();
|
||||||
|
let merged = timings.merge(&other);
|
||||||
|
assert_eq!(merged.data.len(), 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue