From f8a13687656efdd03e4966e3a81bca23fdeef81f Mon Sep 17 00:00:00 2001 From: Tom Van Eyck Date: Sat, 9 Dec 2023 14:43:37 +0100 Subject: [PATCH 1/4] feat: add `cargo today` command (#43) --- .cargo/config.toml | 1 + Cargo.lock | 156 +++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 10 +-- README.md | 35 ++++++++++ src/main.rs | 26 ++++++++ src/template/day.rs | 16 +++++ 6 files changed, 238 insertions(+), 6 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 3530669..96b289c 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,4 +1,5 @@ [alias] +today = "run --quiet --release --features today -- today" scaffold = "run --quiet --release -- scaffold" download = "run --quiet --release -- download" read = "run --quiet --release -- read" diff --git a/Cargo.lock b/Cargo.lock index b444b05..3db9765 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,10 +21,26 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" name = "advent_of_code" version = "0.9.5" dependencies = [ + "chrono", "dhat", "pico-args", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -52,6 +68,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + [[package]] name = "cc" version = "1.0.83" @@ -67,6 +89,26 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "dhat" version = "0.3.2" @@ -89,12 +131,44 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "itoa" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +[[package]] +name = "js-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -117,6 +191,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + [[package]] name = "memchr" version = "2.6.4" @@ -142,6 +222,15 @@ dependencies = [ "sys-info", ] +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.32.1" @@ -153,9 +242,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "parking_lot" @@ -307,6 +396,69 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "wasm-bindgen" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index 0d8b189..371b2bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,14 +10,16 @@ publish = false [lib] doctest = false +[profile.dhat] +inherits = "release" +debug = 1 + [features] +today = ["chrono"] test_lib = [] dhat-heap = ["dhat"] [dependencies] +chrono = { version = "0.4.31", optional = true } pico-args = "0.5.0" dhat = { version = "0.3.2", optional = true } - -[profile.dhat] -inherits = "release" -debug = 1 diff --git a/README.md b/README.md index 043e1be..e138e7e 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,41 @@ cargo read # ...the input... ``` +### Scaffold, download and read in one go + +> [!IMPORTANT] +> This command requires [installing the aoc-cli crate](#configure-aoc-cli-integration). + +During december, the `today` shorthand command can be used to: + + - scaffold a solution for the current day + - download its input + - and read the puzzle + +in one go. + +```sh +# example: `cargo today` on December 1st +cargo today + +# output: +# Created module file "src/bin/01.rs" +# Created empty input file "data/inputs/01.txt" +# Created empty example file "data/examples/01.txt" +# --- +# πŸŽ„ Type `cargo solve 01` to run your solution. +# [INFO aoc] πŸŽ„ aoc-cli - Advent of Code command-line tool +# [INFO aoc_client] πŸŽ… Saved puzzle to 'data/puzzles/01.md' +# [INFO aoc_client] πŸŽ… Saved input to 'data/inputs/01.txt' +# --- +# πŸŽ„ Successfully wrote input to "data/inputs/01.txt". +# πŸŽ„ Successfully wrote puzzle to "data/puzzles/01.md". +# +# Loaded session cookie from "/Users//.adventofcode.session". +# Fetching puzzle for day 1, 2022... +# ...the input... +``` + ## Optional template features ### Configure aoc-cli integration diff --git a/src/main.rs b/src/main.rs index 7eb8b8f..95dcb2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,11 @@ use advent_of_code::template::commands::{all, download, read, scaffold, solve}; use args::{parse, AppArguments}; +#[cfg(feature = "today")] +use advent_of_code::template::Day; +#[cfg(feature = "today")] +use std::process; + mod args { use advent_of_code::template::Day; use std::process; @@ -27,6 +32,8 @@ mod args { release: bool, time: bool, }, + #[cfg(feature = "today")] + Today, } pub fn parse() -> Result> { @@ -54,6 +61,8 @@ mod args { time: args.contains("--time"), dhat: args.contains("--dhat"), }, + #[cfg(feature = "today")] + Some("today") => AppArguments::Today, Some(x) => { eprintln!("Unknown command: {x}"); process::exit(1); @@ -96,6 +105,23 @@ fn main() { dhat, submit, } => solve::handle(day, release, time, dhat, submit), + #[cfg(feature = "today")] + AppArguments::Today => { + match Day::today() { + Some(day) => { + scaffold::handle(day); + download::handle(day); + read::handle(day) + } + None => { + eprintln!( + "`today` command can only be run between the 1st and \ + the 25th of december. Please use `scaffold` with a specific day." + ); + process::exit(1) + } + }; + } }, }; } diff --git a/src/template/day.rs b/src/template/day.rs index fe1f66a..ca264b8 100644 --- a/src/template/day.rs +++ b/src/template/day.rs @@ -2,6 +2,9 @@ use std::error::Error; use std::fmt::Display; use std::str::FromStr; +#[cfg(feature = "today")] +use chrono::{Datelike, Local}; + /// A valid day number of advent (i.e. an integer in range 1 to 25). /// /// # Display @@ -37,6 +40,19 @@ impl Day { } } +#[cfg(feature = "today")] +impl Day { + /// Returns the current day if it's between the 1st and the 25th of december, `None` otherwise. + pub fn today() -> Option { + let today = Local::now(); + if today.month() == 12 && today.day() <= 25 { + Self::new(u8::try_from(today.day()).ok()?) + } else { + None + } + } +} + impl Display for Day { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:02}", self.0) From 4c4232139aa32ad58890dd84c42647cf0537d500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sp=C3=B6ttel?= <1682504+fspoettel@users.noreply.github.com> Date: Sat, 9 Dec 2023 15:12:52 +0100 Subject: [PATCH 2/4] docs: improve skimmability --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e138e7e..0d72d22 100644 --- a/README.md +++ b/README.md @@ -14,14 +14,14 @@ Solutions for [Advent of Code](https://adventofcode.com/) in [Rust](https://www. This template supports all major OS (macOS, Linux, Windows). -### Create your repository πŸ“ +### πŸ“ Create your repository 1. Open [the template repository](https://github.com/fspoettel/advent-of-code-rust) on Github. 2. Click [Use this template](https://github.com/fspoettel/advent-of-code-rust/generate) and create your repository. 3. Clone your repository to your computer. 4. If you are solving a previous year's advent of code, change the `AOC_YEAR` variable in `.cargo/config.toml` to reflect the year you are solving. -### Setup rust πŸ’» +### πŸ’» Setup rust 1. Install the [Rust toolchain](https://www.rust-lang.org/tools/install). 2. (recommended) Install the [rust-analyzer](https://rust-analyzer.github.io/manual.html) extension for your code editor. @@ -33,7 +33,7 @@ This template supports all major OS (macOS, Linux, Windows). ## Usage -### Scaffold a day +### ➑️ Scaffold a day ```sh # example: `cargo scaffold 1` @@ -54,12 +54,12 @@ Every [solution](https://github.com/fspoettel/advent-of-code-rust/blob/main/src/ > [!TIP] > If a day has different example inputs for both parts, you can use the `read_file_part()` helper in your tests instead of `read_file()`. For example, if this applies to day 1, you can create a second example file `01-2.txt` and invoke the helper like `let result = part_two(&advent_of_code::template::read_file_part("examples", DAY, 2));` to read it in `test_part_two`. -### Download input & description for a day +### ➑️ Download input for a day > [!IMPORTANT] > This requires [installing the aoc-cli crate](#configure-aoc-cli-integration). -You can automatically download puzzle inputs and description by either appending the `--download` flag to `scaffold` (e.g. `cargo scaffold 4 --download`) or with the separate `download` command: +You can automatically download puzzle input and description by either appending the `--download` flag to `scaffold` (e.g. `cargo scaffold 4 --download`) or with the separate `download` command: ```sh # example: `cargo download 1` @@ -74,7 +74,7 @@ cargo download # πŸŽ„ Successfully wrote puzzle to "data/puzzles/01.md". ``` -### Run solutions for a day +### ➑️ Run solutions for a day ```sh # example: `cargo solve 01` @@ -100,7 +100,7 @@ For example, running a benchmarked, optimized execution of day 1 would look like In order to submit part of a solution for checking, append the `--submit ` option to the `solve` command. -### Run all solutions +### ➑️ Run all solutions ```sh cargo all @@ -124,7 +124,7 @@ The template can output a table with solution times to your readme. In order to 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 ```sh cargo test @@ -132,19 +132,19 @@ cargo test To run tests for a specific day, append `--bin `, e.g. `cargo test --bin 01`. You can further scope it down to a specific part, e.g. `cargo test --bin 01 part_one`. -### Format code +### ➑️ Format code ```sh cargo fmt ``` -### Lint code +### ➑️ Lint code ```sh cargo clippy ``` -### Read puzzle description in terminal +### ➑️ Read puzzle description > [!IMPORTANT] > This command requires [installing the aoc-cli crate](#configure-aoc-cli-integration). @@ -159,7 +159,7 @@ cargo read # ...the input... ``` -### Scaffold, download and read in one go +### ➑️ Scaffold, download & read the current aoc day > [!IMPORTANT] > This command requires [installing the aoc-cli crate](#configure-aoc-cli-integration). From 874f57b3591058f776344c7b7e478425b97344ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sp=C3=B6ttel?= <1682504+fspoettel@users.noreply.github.com> Date: Sun, 10 Dec 2023 13:55:17 +0100 Subject: [PATCH 3/4] feat: make `cargo time` incremental by default (#53) Co-authored-by: Tristan Guichaoua <33934311+tguichaoua@users.noreply.github.com> --- .cargo/config.toml | 2 +- .gitignore | 4 + Cargo.lock | 7 + Cargo.toml | 5 +- README.md | 10 +- src/main.rs | 15 +- src/template/aoc_cli.rs | 2 - src/template/commands/all.rs | 253 +------------------ src/template/commands/mod.rs | 1 + src/template/commands/time.rs | 35 +++ src/template/mod.rs | 7 +- src/template/readme_benchmarks.rs | 68 +++--- src/template/run_multi.rs | 255 +++++++++++++++++++ src/template/runner.rs | 4 +- src/template/timings.rs | 391 ++++++++++++++++++++++++++++++ 15 files changed, 758 insertions(+), 301 deletions(-) create mode 100644 src/template/commands/time.rs create mode 100644 src/template/run_multi.rs create mode 100644 src/template/timings.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 96b289c..66b873e 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -6,7 +6,7 @@ read = "run --quiet --release -- read" solve = "run --quiet --release -- solve" all = "run --quiet --release -- all" -time = "run --quiet --release -- all --release --time" +time = "run --quiet --release -- time" [env] AOC_YEAR = "2023" diff --git a/.gitignore b/.gitignore index 3b6ae09..216820d 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,7 @@ data/puzzles/* # Dhat dhat-heap.json + +# Benchmarks + +data/timings.json diff --git a/Cargo.lock b/Cargo.lock index 3db9765..bff5d0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,7 @@ dependencies = [ "chrono", "dhat", "pico-args", + "tinyjson", ] [[package]] @@ -390,6 +391,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" +[[package]] +name = "tinyjson" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab95735ea2c8fd51154d01e39cf13912a78071c2d89abc49a7ef102a7dd725a" + [[package]] name = "unicode-ident" version = "1.0.12" diff --git a/Cargo.toml b/Cargo.toml index 371b2bc..9120a8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,11 +15,12 @@ inherits = "release" debug = 1 [features] +dhat-heap = ["dhat"] today = ["chrono"] test_lib = [] -dhat-heap = ["dhat"] [dependencies] chrono = { version = "0.4.31", optional = true } -pico-args = "0.5.0" dhat = { version = "0.3.2", optional = true } +pico-args = "0.5.0" +tinyjson = "2" diff --git a/README.md b/README.md index 0d72d22..472f8ba 100644 --- a/README.md +++ b/README.md @@ -116,13 +116,15 @@ cargo all # 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 `. + +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 diff --git a/src/main.rs b/src/main.rs index 95dcb2e..57d4fe3 100644 --- a/src/main.rs +++ b/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}; #[cfg(feature = "today")] @@ -32,6 +32,10 @@ mod args { release: bool, time: bool, }, + Time { + all: bool, + day: Option, + }, #[cfg(feature = "today")] Today, } @@ -44,6 +48,14 @@ mod args { release: args.contains("--release"), time: args.contains("--time"), }, + Some("time") => { + let all = args.contains("--all"); + + AppArguments::Time { + all, + day: args.opt_free_from_str()?, + } + } Some("download") => AppArguments::Download { day: args.free_from_str()?, }, @@ -90,6 +102,7 @@ fn main() { } Ok(args) => match args { AppArguments::All { release, time } => all::handle(release, time), + AppArguments::Time { day, all } => time::handle(day, all), AppArguments::Download { day } => download::handle(day), AppArguments::Read { day } => read::handle(day), AppArguments::Scaffold { day, download } => { diff --git a/src/template/aoc_cli.rs b/src/template/aoc_cli.rs index a9ff3b0..2d3300d 100644 --- a/src/template/aoc_cli.rs +++ b/src/template/aoc_cli.rs @@ -11,7 +11,6 @@ pub enum AocCommandError { CommandNotFound, CommandNotCallable, BadExitStatus(Output), - IoError, } impl Display for AocCommandError { @@ -22,7 +21,6 @@ impl Display for AocCommandError { AocCommandError::BadExitStatus(_) => { write!(f, "aoc-cli exited with a non-zero status.") } - AocCommandError::IoError => write!(f, "could not write output files to file system."), } } } diff --git a/src/template/commands/all.rs b/src/template/commands/all.rs index a183445..444497d 100644 --- a/src/template/commands/all.rs +++ b/src/template/commands/all.rs @@ -1,254 +1,5 @@ -use std::io; - -use crate::template::{ - all_days, - readme_benchmarks::{self, Timings}, - Day, ANSI_BOLD, ANSI_ITALIC, ANSI_RESET, -}; +use crate::template::{all_days, run_multi::run_multi}; pub fn handle(is_release: bool, is_timed: bool) { - let mut timings: Vec = vec![]; - - 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::() / 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 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, 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 { - 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::().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); - } - } + run_multi(all_days().collect(), is_release, is_timed); } diff --git a/src/template/commands/mod.rs b/src/template/commands/mod.rs index 88f4696..36be280 100644 --- a/src/template/commands/mod.rs +++ b/src/template/commands/mod.rs @@ -3,3 +3,4 @@ pub mod download; pub mod read; pub mod scaffold; pub mod solve; +pub mod time; diff --git a/src/template/commands/time.rs b/src/template/commands/time.rs new file mode 100644 index 0000000..ce110ab --- /dev/null +++ b/src/template/commands/time.rs @@ -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, 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."); + } + } +} diff --git a/src/template/mod.rs b/src/template/mod.rs index 4d84a72..dd8e4c0 100644 --- a/src/template/mod.rs +++ b/src/template/mod.rs @@ -2,12 +2,15 @@ use std::{env, fs}; pub mod aoc_cli; pub mod commands; -mod day; -pub mod readme_benchmarks; pub mod runner; pub use day::*; +mod day; +mod readme_benchmarks; +mod run_multi; +mod timings; + pub const ANSI_ITALIC: &str = "\x1b[3m"; pub const ANSI_BOLD: &str = "\x1b[1m"; pub const ANSI_RESET: &str = "\x1b[0m"; diff --git a/src/template/readme_benchmarks.rs b/src/template/readme_benchmarks.rs index b282196..1498dbb 100644 --- a/src/template/readme_benchmarks.rs +++ b/src/template/readme_benchmarks.rs @@ -2,6 +2,7 @@ /// The approach taken is similar to how `aoc-readme-stars` handles this. use std::{fs, io}; +use crate::template::timings::Timings; use crate::template::Day; static MARKER: &str = ""; @@ -18,14 +19,6 @@ impl From for Error { } } -#[derive(Clone)] -pub struct Timings { - pub day: Day, - pub part_1: Option, - pub part_2: Option, - pub total_nanos: f64, -} - pub struct TablePosition { pos_start: usize, pos_end: usize, @@ -58,7 +51,7 @@ fn locate_table(readme: &str) -> Result { Ok(TablePosition { pos_start, pos_end }) } -fn construct_table(prefix: &str, timings: Vec, total_millis: f64) -> String { +fn construct_table(prefix: &str, timings: Timings, total_millis: f64) -> String { let header = format!("{prefix} Benchmarks"); let mut lines: Vec = vec![ @@ -69,7 +62,7 @@ fn construct_table(prefix: &str, timings: Vec, total_millis: f64) -> St "| :---: | :---: | :---: |".into(), ]; - for timing in timings { + for timing in timings.data { let path = get_path_for_bin(timing.day); lines.push(format!( "| [Day {}]({}) | `{}` | `{}` |", @@ -87,16 +80,17 @@ fn construct_table(prefix: &str, timings: Vec, total_millis: f64) -> St lines.join("\n") } -fn update_content(s: &mut String, timings: Vec, total_millis: f64) -> Result<(), Error> { +fn update_content(s: &mut String, timings: Timings, total_millis: f64) -> Result<(), Error> { let positions = locate_table(s)?; let table = construct_table("##", timings, total_millis); s.replace_range(positions.pos_start..positions.pos_end, &table); Ok(()) } -pub fn update(timings: Vec, total_millis: f64) -> Result<(), Error> { +pub fn update(timings: Timings) -> Result<(), Error> { let path = "README.md"; 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)?; fs::write(path, &readme)?; Ok(()) @@ -104,30 +98,32 @@ pub fn update(timings: Vec, total_millis: f64) -> Result<(), Error> { #[cfg(feature = "test_lib")] mod tests { - use super::{update_content, Timings, MARKER}; - use crate::day; - - fn get_mock_timings() -> Vec { - vec![ - Timings { - day: day!(1), - part_1: Some("10ms".into()), - part_2: Some("20ms".into()), - total_nanos: 3e+10, - }, - Timings { - day: day!(2), - part_1: Some("30ms".into()), - part_2: Some("40ms".into()), - total_nanos: 7e+10, - }, - Timings { - day: day!(4), - part_1: Some("40ms".into()), - part_2: Some("50ms".into()), - total_nanos: 9e+10, - }, - ] + use super::{update_content, MARKER}; + use crate::{day, template::timings::Timing, template::timings::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: Some("50ms".into()), + total_nanos: 9e+10, + }, + ], + } } #[test] diff --git a/src/template/run_multi.rs b/src/template/run_multi.rs new file mode 100644 index 0000000..0fdf2d5 --- /dev/null +++ b/src/template/run_multi.rs @@ -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, is_release: bool, is_timed: bool) -> Option { + let mut timings: Vec = 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 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, 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 { + 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::().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); + } + } +} diff --git a/src/template/runner.rs b/src/template/runner.rs index 021c9cd..b4e41bc 100644 --- a/src/template/runner.rs +++ b/src/template/runner.rs @@ -1,5 +1,4 @@ /// Encapsulates code that interacts with solution functions. -use crate::template::{aoc_cli, Day, ANSI_ITALIC, ANSI_RESET}; use std::fmt::Display; use std::hint::black_box; use std::io::{stdout, Write}; @@ -7,7 +6,8 @@ use std::process::Output; use std::time::{Duration, Instant}; 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(func: impl Fn(I) -> Option, input: I, day: Day, part: u8) { let part_str = format!("Part {part}"); diff --git a/src/template/timings.rs b/src/template/timings.rs new file mode 100644 index 0000000..f7dc091 --- /dev/null +++ b/src/template/timings.rs @@ -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, + pub part_2: Option, + 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, +} + +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 = 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::() / 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 for JsonValue { + fn from(value: Timings) -> Self { + let mut map: HashMap = HashMap::new(); + + map.insert( + "data".into(), + JsonValue::Array(value.data.iter().map(JsonValue::from).collect()), + ); + + JsonValue::Object(map) + } +} + +impl TryFrom for Timings { + type Error = String; + + fn try_from(value: String) -> Result { + let json = JsonValue::from_str(&value).or(Err("not valid JSON file."))?; + + let json_data = json + .get::>() + .ok_or("expected JSON document to be an object.")? + .get("data") + .ok_or("expected JSON document to have key `data`.")? + .get::>() + .ok_or("expected `json.data` to be an array.")?; + + Ok(Timings { + data: json_data + .iter() + .map(Timing::try_from) + .collect::>()?, + }) + } +} + +/* -------------------------------------------------------------------------- */ + +impl From<&Timing> for JsonValue { + fn from(value: &Timing) -> Self { + let mut map: HashMap = 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 { + let json = value + .get::>() + .ok_or("Expected timing to be a JSON object.")?; + + let day = json + .get("day") + .and_then(|v| v.get::()) + .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::() }) + .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::() }) + .ok_or("Expected timing.part_2 to be null or string.")?; + + let total_nanos = json + .get("total_nanos") + .and_then(|v| v.get::().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::>() + .unwrap() + .get("data") + .unwrap() + .get::>() + .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); + } + } +} From c9671558ac18a20fda584a9a726a2c8c0a16849a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sp=C3=B6ttel?= <1682504+fspoettel@users.noreply.github.com> Date: Sun, 10 Dec 2023 13:58:22 +0100 Subject: [PATCH 4/4] chore(release): bump version --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bff5d0a..6111560 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "advent_of_code" -version = "0.9.5" +version = "0.10.0" dependencies = [ "chrono", "dhat", diff --git a/Cargo.toml b/Cargo.toml index 9120a8d..584e150 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "advent_of_code" -version = "0.9.5" +version = "0.10.0" authors = ["Felix SpΓΆttel <1682504+fspoettel@users.noreply.github.com>"] edition = "2021" default-run = "advent_of_code"