feat: implement benching, refactor template

This commit is contained in:
Felix Spöttel 2023-10-21 17:54:42 +02:00
parent d10ec0573e
commit be568dc6dd
20 changed files with 952 additions and 524 deletions

View file

@ -1,10 +1,11 @@
[alias]
scaffold = "run --bin scaffold --quiet --release -- "
download = "run --bin download --quiet --release -- "
read = "run --bin read --quiet --release -- "
scaffold = "run --quiet --release -- scaffold"
download = "run --quiet --release -- download"
read = "run --quiet --release -- read"
solve = "run --bin solve --quiet --release -- "
all = "run"
solve = "run --quiet --release -- solve"
all = "run --quiet --release -- all"
time = "run --quiet --release -- all --release --time"
[env]
AOC_YEAR = "2022"

6
.gitignore vendored
View file

@ -16,5 +16,7 @@ target/
# Advent of Code
# @see https://old.reddit.com/r/adventofcode/comments/k99rod/sharing_input_data_were_we_requested_not_to/gf2ukkf/?context=3
/src/inputs
!/src/inputs/.keep
/data
!/data/inputs/.keep
!/data/examples/.keep
!/data/puzzles/.keep

View file

@ -6,6 +6,8 @@ Solutions for [Advent of Code](https://adventofcode.com/) in [Rust](https://www.
<!--- advent_readme_stars table --->
<!--- benchmarking table --->
---
## Template setup

0
src/bin/.keep Normal file
View file

View file

@ -1,44 +0,0 @@
/*
* This file contains template code.
* There is no need to edit this file unless you want to change template functionality.
*/
use advent_of_code::aoc_cli;
use std::process;
struct Args {
day: u8,
}
fn parse_args() -> Result<Args, pico_args::Error> {
let mut args = pico_args::Arguments::from_env();
Ok(Args {
day: args.free_from_str()?,
})
}
fn main() {
let args = match parse_args() {
Ok(args) => args,
Err(e) => {
eprintln!("Failed to process arguments: {e}");
process::exit(1);
}
};
if aoc_cli::check().is_err() {
eprintln!("command \"aoc\" not found or not callable. Try running \"cargo install aoc-cli\" to install it.");
process::exit(1);
}
match aoc_cli::download(args.day) {
Ok(cmd_output) => {
if !cmd_output.status.success() {
process::exit(1);
}
}
Err(e) => {
eprintln!("failed to spawn aoc-cli: {e}");
process::exit(1);
}
}
}

View file

@ -1,44 +0,0 @@
/*
* This file contains template code.
* There is no need to edit this file unless you want to change template functionality.
*/
use advent_of_code::aoc_cli;
use std::process;
struct Args {
day: u8,
}
fn parse_args() -> Result<Args, pico_args::Error> {
let mut args = pico_args::Arguments::from_env();
Ok(Args {
day: args.free_from_str()?,
})
}
fn main() {
let args = match parse_args() {
Ok(args) => args,
Err(e) => {
eprintln!("Failed to process arguments: {e}");
process::exit(1);
}
};
if aoc_cli::check().is_err() {
eprintln!("command \"aoc\" not found or not callable. Try running \"cargo install aoc-cli\" to install it.");
process::exit(1);
}
match aoc_cli::read(args.day) {
Ok(cmd_output) => {
if !cmd_output.status.success() {
process::exit(1);
}
}
Err(e) => {
eprintln!("failed to spawn aoc-cli: {e}");
process::exit(1);
}
}
}

View file

@ -1,58 +0,0 @@
/*
* This file contains template code.
* There is no need to edit this file unless you want to change template functionality.
*/
use std::process::{self, Command, Stdio};
struct Args {
day: u8,
release: bool,
submit: Option<u8>,
}
fn parse_args() -> Result<Args, pico_args::Error> {
let mut args = pico_args::Arguments::from_env();
Ok(Args {
day: args.free_from_str()?,
release: args.contains("--release"),
submit: args.opt_value_from_str("--submit")?,
})
}
fn run_solution(day: u8, release: bool, submit_part: Option<u8>) -> Result<(), std::io::Error> {
let day_padded = format!("{:02}", day);
let mut cmd_args = vec!["run".to_string(), "--bin".to_string(), day_padded];
if release {
cmd_args.push("--release".to_string());
}
if let Some(submit_part) = submit_part {
cmd_args.push("--".to_string());
cmd_args.push("--submit".to_string());
cmd_args.push(submit_part.to_string())
}
let mut cmd = Command::new("cargo")
.args(&cmd_args)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()?;
cmd.wait()?;
Ok(())
}
fn main() {
let args = match parse_args() {
Ok(args) => args,
Err(e) => {
eprintln!("Failed to process arguments: {}", e);
process::exit(1);
}
};
run_solution(args.day, args.release, args.submit).unwrap();
}

View file

@ -1,4 +0,0 @@
/*
* Use this file if you want to extract helpers from your solutions.
* Example import from this file: `use advent_of_code::helpers::example_fn;`.
*/

View file

@ -1,290 +1 @@
/*
* This file contains template code.
* There is no need to edit this file unless you want to change template functionality.
* Prefer `./helpers.rs` if you want to extract code from your solutions.
*/
use std::env;
use std::fs;
pub mod helpers;
pub const ANSI_ITALIC: &str = "\x1b[3m";
pub const ANSI_BOLD: &str = "\x1b[1m";
pub const ANSI_RESET: &str = "\x1b[0m";
#[macro_export]
macro_rules! solve {
($day:expr, $part:expr, $solver:ident, $input:expr) => {{
use advent_of_code::{ANSI_BOLD, ANSI_ITALIC, ANSI_RESET, aoc_cli};
use std::fmt::Display;
use std::time::Instant;
use std::env;
use std::process;
fn submit_if_requested<T: Display>(result: T) {
let args: Vec<String> = env::args().collect();
if args.contains(&"--submit".into()) {
if aoc_cli::check().is_err() {
eprintln!("command \"aoc\" not found or not callable. Try running \"cargo install aoc-cli\" to install it.");
process::exit(1);
}
if args.len() < 3 {
eprintln!("Unexpected command-line input. Format: cargo solve 1 --submit 1");
process::exit(1);
}
let part_index = args.iter().position(|x| x == "--submit").unwrap() + 1;
let part_submit = match args[part_index].parse::<u8>() {
Ok(x) => x,
Err(_) => {
eprintln!("Unexpected command-line input. Format: cargo solve 1 --submit 1");
process::exit(1);
}
};
if part_submit == $part {
println!("Submitting puzzle answer for part {}...", $part);
aoc_cli::submit($day, $part, result).unwrap();
}
}
}
fn print_result<T: Display>(func: impl FnOnce(&str) -> Option<T>, input: &str) {
let timer = Instant::now();
let result = func(input);
let elapsed = timer.elapsed();
match result {
Some(result) => {
println!(
"{} {}(elapsed: {:.2?}){}",
result, ANSI_ITALIC, elapsed, ANSI_RESET
);
submit_if_requested(result);
}
None => {
println!("not solved.")
}
}
}
println!("🎄 {}Part {}{} 🎄", ANSI_BOLD, $part, ANSI_RESET);
print_result($solver, $input);
}};
}
pub fn read_file(folder: &str, day: u8) -> String {
let cwd = env::current_dir().unwrap();
let filepath = cwd.join("src").join(folder).join(format!("{day:02}.txt"));
let f = fs::read_to_string(filepath);
f.expect("could not open input file")
}
fn parse_time(val: &str, postfix: &str) -> f64 {
val.split(postfix).next().unwrap().parse().unwrap()
}
pub fn parse_exec_time(output: &str) -> f64 {
output.lines().fold(0_f64, |acc, l| {
if !l.contains("elapsed:") {
acc
} else {
let timing = l.split("(elapsed: ").last().unwrap();
// use `contains` istd. of `ends_with`: string may contain ANSI escape sequences.
// for possible time formats, see: https://github.com/rust-lang/rust/blob/1.64.0/library/core/src/time.rs#L1176-L1200
if timing.contains("ns)") {
acc // range below rounding precision.
} else if timing.contains("µs)") {
acc + parse_time(timing, "µs") / 1000_f64
} else if timing.contains("ms)") {
acc + parse_time(timing, "ms")
} else if timing.contains("s)") {
acc + parse_time(timing, "s") * 1000_f64
} else {
acc
}
}
})
}
/// copied from: https://github.com/rust-lang/rust/blob/1.64.0/library/std/src/macros.rs#L328-L333
#[cfg(test)]
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(test)]
mod tests {
use super::*;
#[test]
fn test_parse_exec_time() {
assert_approx_eq!(
parse_exec_time(&format!(
"🎄 Part 1 🎄\n0 (elapsed: 74.13ns){}\n🎄 Part 2 🎄\n0 (elapsed: 50.00ns){}",
ANSI_RESET, ANSI_RESET
)),
0_f64
);
assert_approx_eq!(
parse_exec_time("🎄 Part 1 🎄\n0 (elapsed: 755µs)\n🎄 Part 2 🎄\n0 (elapsed: 700µs)"),
1.455_f64
);
assert_approx_eq!(
parse_exec_time("🎄 Part 1 🎄\n0 (elapsed: 70µs)\n🎄 Part 2 🎄\n0 (elapsed: 1.45ms)"),
1.52_f64
);
assert_approx_eq!(
parse_exec_time(
"🎄 Part 1 🎄\n0 (elapsed: 10.3s)\n🎄 Part 2 🎄\n0 (elapsed: 100.50ms)"
),
10400.50_f64
);
}
}
pub mod aoc_cli {
use std::{
fmt::Display,
process::{Command, Output, Stdio},
};
#[derive(Debug)]
pub enum AocCliError {
CommandNotFound,
CommandNotCallable,
BadExitStatus(Output),
IoError,
}
impl Display for AocCliError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AocCliError::CommandNotFound => write!(f, "aoc-cli is not present in environment."),
AocCliError::CommandNotCallable => write!(f, "aoc-cli could not be called."),
AocCliError::BadExitStatus(_) => {
write!(f, "aoc-cli exited with a non-zero status.")
}
AocCliError::IoError => write!(f, "could not write output files to file system."),
}
}
}
pub fn check() -> Result<(), AocCliError> {
Command::new("aoc")
.arg("-V")
.output()
.map_err(|_| AocCliError::CommandNotFound)?;
Ok(())
}
pub fn read(day: u8) -> Result<Output, AocCliError> {
// TODO: output local puzzle if present.
let puzzle_path = get_puzzle_path(day);
let args = build_args(
"read",
&[
"--description-only".into(),
"--puzzle-file".into(),
puzzle_path,
],
day,
);
call_aoc_cli(&args)
}
pub fn download(day: u8) -> Result<Output, AocCliError> {
let input_path = get_input_path(day);
let puzzle_path = get_puzzle_path(day);
let args = build_args(
"download",
&[
"--overwrite".into(),
"--input-file".into(),
input_path.to_string(),
"--puzzle-file".into(),
puzzle_path.to_string(),
],
day,
);
let output = call_aoc_cli(&args)?;
if output.status.success() {
println!("---");
println!("🎄 Successfully wrote input to \"{}\".", &input_path);
println!("🎄 Successfully wrote puzzle to \"{}\".", &puzzle_path);
Ok(output)
} else {
Err(AocCliError::BadExitStatus(output))
}
}
pub fn submit<T: Display>(day: u8, part: u8, result: T) -> Result<Output, AocCliError> {
// workaround: the argument order is inverted for submit.
let mut args = build_args("submit", &[], day);
args.push(part.to_string());
args.push(result.to_string());
call_aoc_cli(&args)
}
fn get_input_path(day: u8) -> String {
let day_padded = format!("{day:02}");
format!("src/inputs/{day_padded}.txt")
}
fn get_puzzle_path(day: u8) -> String {
let day_padded = format!("{day:02}");
format!("src/puzzles/{day_padded}.md")
}
fn get_year() -> Option<u16> {
match std::env::var("AOC_YEAR") {
Ok(x) => x.parse().ok().or(None),
Err(_) => None,
}
}
fn build_args(command: &str, args: &[String], day: u8) -> Vec<String> {
let mut cmd_args = args.to_vec();
if let Some(year) = get_year() {
cmd_args.push("--year".into());
cmd_args.push(year.to_string());
}
cmd_args.append(&mut vec!["--day".into(), day.to_string(), command.into()]);
cmd_args
}
fn call_aoc_cli(args: &[String]) -> Result<Output, AocCliError> {
if cfg!(debug_assertions) {
println!("Calling >aoc with: {}", args.join(" "));
}
Command::new("aoc")
.args(args)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(|_| AocCliError::CommandNotCallable)
}
}
pub mod template;

View file

@ -1,45 +1,93 @@
/*
* This file contains template code.
* There is no need to edit this file unless you want to change template functionality.
*/
use advent_of_code::{ANSI_BOLD, ANSI_ITALIC, ANSI_RESET};
use std::process::Command;
use advent_of_code::template::commands::{
all::all_handler, download::download_handler, read::read_handler, scaffold::scaffold_handler,
solve::solve_handler,
};
use args::{parse_args, AppArgs};
mod args {
use std::process;
pub enum AppArgs {
Download {
day: u8,
},
Read {
day: u8,
},
Scaffold {
day: u8,
},
Solve {
day: u8,
release: bool,
time: bool,
submit: Option<u8>,
},
All {
release: bool,
time: bool,
},
}
pub fn parse_args() -> Result<AppArgs, Box<dyn std::error::Error>> {
let mut args = pico_args::Arguments::from_env();
let app_args = match args.subcommand()?.as_deref() {
Some("all") => AppArgs::All {
release: args.contains("--release"),
time: args.contains("--time"),
},
Some("download") => AppArgs::Download {
day: args.free_from_str()?,
},
Some("read") => AppArgs::Read {
day: args.free_from_str()?,
},
Some("scaffold") => AppArgs::Scaffold {
day: args.free_from_str()?,
},
Some("solve") => AppArgs::Solve {
day: args.free_from_str()?,
release: args.contains("--release"),
submit: args.opt_value_from_str("--submit")?,
time: args.contains("--time"),
},
Some(x) => {
eprintln!("Unknown command: {}", x);
process::exit(1);
}
None => {
eprintln!("No command specified.");
process::exit(1);
}
};
let remaining = args.finish();
if !remaining.is_empty() {
eprintln!("Warning: unknown argument(s): {:?}.", remaining);
}
Ok(app_args)
}
}
fn main() {
let total: f64 = (1..=25)
.map(|day| {
let day = format!("{day:02}");
let mut args = vec!["run", "--bin", &day];
if cfg!(not(debug_assertions)) {
args.push("--release");
}
let cmd = Command::new("cargo").args(&args).output().unwrap();
println!("----------");
println!("{ANSI_BOLD}| Day {day} |{ANSI_RESET}");
println!("----------");
let output = String::from_utf8(cmd.stdout).unwrap();
let is_empty = output.is_empty();
println!(
"{}",
if is_empty {
"Not solved."
} else {
output.trim()
}
);
if is_empty {
0_f64
} else {
advent_of_code::parse_exec_time(&output)
}
})
.sum();
println!("{ANSI_BOLD}Total:{ANSI_RESET} {ANSI_ITALIC}{total:.2}ms{ANSI_RESET}");
match parse_args() {
Err(err) => {
eprintln!("Error: {}", err);
std::process::exit(1);
}
Ok(args) => match args {
AppArgs::All { release, time } => all_handler(release, time),
AppArgs::Download { day } => download_handler(day),
AppArgs::Read { day } => read_handler(day),
AppArgs::Scaffold { day } => scaffold_handler(day),
AppArgs::Solve {
day,
release,
time,
submit,
} => solve_handler(day, release, time, submit),
},
};
}

127
src/template/aoc_cli.rs Normal file
View file

@ -0,0 +1,127 @@
/// Wrapper module around the "aoc-cli" command-line.
use std::{
fmt::Display,
process::{Command, Output, Stdio},
};
#[derive(Debug)]
pub enum AocCliError {
CommandNotFound,
CommandNotCallable,
BadExitStatus(Output),
IoError,
}
impl Display for AocCliError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AocCliError::CommandNotFound => write!(f, "aoc-cli is not present in environment."),
AocCliError::CommandNotCallable => write!(f, "aoc-cli could not be called."),
AocCliError::BadExitStatus(_) => {
write!(f, "aoc-cli exited with a non-zero status.")
}
AocCliError::IoError => write!(f, "could not write output files to file system."),
}
}
}
pub fn check() -> Result<(), AocCliError> {
Command::new("aoc")
.arg("-V")
.output()
.map_err(|_| AocCliError::CommandNotFound)?;
Ok(())
}
pub fn read(day: u8) -> Result<Output, AocCliError> {
let puzzle_path = get_puzzle_path(day);
let args = build_args(
"read",
&[
"--description-only".into(),
"--puzzle-file".into(),
puzzle_path,
],
day,
);
call_aoc_cli(&args)
}
pub fn download(day: u8) -> Result<Output, AocCliError> {
let input_path = get_input_path(day);
let puzzle_path = get_puzzle_path(day);
let args = build_args(
"download",
&[
"--overwrite".into(),
"--input-file".into(),
input_path.to_string(),
"--puzzle-file".into(),
puzzle_path.to_string(),
],
day,
);
let output = call_aoc_cli(&args)?;
println!("---");
println!("🎄 Successfully wrote input to \"{}\".", &input_path);
println!("🎄 Successfully wrote puzzle to \"{}\".", &puzzle_path);
Ok(output)
}
pub fn submit(day: u8, part: u8, result: &str) -> Result<Output, AocCliError> {
// workaround: the argument order is inverted for submit.
let mut args = build_args("submit", &[], day);
args.push(part.to_string());
args.push(result.to_string());
call_aoc_cli(&args)
}
fn get_input_path(day: u8) -> String {
let day_padded = format!("{:02}", day);
format!("data/inputs/{}.txt", day_padded)
}
fn get_puzzle_path(day: u8) -> String {
let day_padded = format!("{:02}", day);
format!("data/puzzles/{}.md", day_padded)
}
fn get_year() -> Option<u16> {
match std::env::var("AOC_YEAR") {
Ok(x) => x.parse().ok().or(None),
Err(_) => None,
}
}
fn build_args(command: &str, args: &[String], day: u8) -> Vec<String> {
let mut cmd_args = args.to_vec();
if let Some(year) = get_year() {
cmd_args.push("--year".into());
cmd_args.push(year.to_string());
}
cmd_args.append(&mut vec!["--day".into(), day.to_string(), command.into()]);
cmd_args
}
fn call_aoc_cli(args: &[String]) -> Result<Output, AocCliError> {
// println!("Calling >aoc with: {}", args.join(" "));
let output = Command::new("aoc")
.args(args)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(|_| AocCliError::CommandNotCallable)?;
if output.status.success() {
Ok(output)
} else {
Err(AocCliError::BadExitStatus(output))
}
}

View file

@ -0,0 +1,261 @@
use std::io;
use crate::template::{
readme_benchmarks::{self, Timings},
ANSI_BOLD, ANSI_ITALIC, ANSI_RESET,
};
pub fn all_handler(is_release: bool, is_timed: bool) {
let mut timings: Vec<Timings> = vec![];
(1..=25).for_each(|day| {
if day > 1 {
println!();
}
println!("{}Day {}{}", ANSI_BOLD, 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>() / 1000000_f64;
println!(
"\n{}Total:{} {}{:.2}ms{}",
ANSI_BOLD, ANSI_RESET, ANSI_ITALIC, total_millis, 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)
}
}
pub fn get_path_for_bin(day: usize) -> String {
let day_padded = format!("{:02}", day);
format!("./src/bin/{}.rs", day_padded)
}
/// 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 std::{
io::{BufRead, BufReader},
path::Path,
process::{Command, Stdio},
thread,
};
/// Run the solution bin for a given day
pub fn run_solution(
day: usize,
is_timed: bool,
is_release: bool,
) -> Result<Vec<String>, Error> {
let day_padded = format!("{:02}", day);
// skip command invocation for days that have not been scaffolded yet.
if !Path::new(&get_path_for_bin(day)).exists() {
return Ok(vec![]);
}
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: usize) -> 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 (timing_str, nanos) = match parse_time(l) {
Some(v) => v,
None => {
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 * 1000000_f64),
s => parse_to_float(s, "s").map(|x| x * 1000000000_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(test)]
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(test)]
mod tests {
use super::parse_exec_time;
#[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(),
],
1,
);
assert_approx_eq!(res.total_nanos, 74137374.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(),
],
1,
);
assert_approx_eq!(res.total_nanos, 3100000000_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(),
],
1,
);
assert_approx_eq!(res.total_nanos, 1000000_f64);
assert_eq!(res.part_1.is_none(), true);
assert_eq!(res.part_2.is_none(), true);
}
}
}

View file

@ -0,0 +1,14 @@
use crate::template::aoc_cli;
use std::process;
pub fn download_handler(day: u8) {
if aoc_cli::check().is_err() {
eprintln!("command \"aoc\" not found or not callable. Try running \"cargo install aoc-cli\" to install it.");
process::exit(1);
}
if let Err(e) = aoc_cli::download(day) {
eprintln!("failed to call aoc-cli: {}", e);
process::exit(1);
};
}

View file

@ -0,0 +1,5 @@
pub mod all;
pub mod download;
pub mod read;
pub mod scaffold;
pub mod solve;

View file

@ -0,0 +1,15 @@
use std::process;
use crate::template::aoc_cli;
pub fn read_handler(day: u8) {
if aoc_cli::check().is_err() {
eprintln!("command \"aoc\" not found or not callable. Try running \"cargo install aoc-cli\" to install it.");
process::exit(1);
}
if let Err(e) = aoc_cli::read(day) {
eprintln!("failed to call aoc-cli: {}", e);
process::exit(1);
};
}

View file

@ -1,7 +1,3 @@
/*
* This file contains template code.
* There is no need to edit this file unless you want to change template functionality.
*/
use std::{
fs::{File, OpenOptions},
io::Write,
@ -16,11 +12,7 @@ pub fn part_two(input: &str) -> Option<u32> {
None
}
fn main() {
let input = &advent_of_code::read_file("inputs", DAY);
advent_of_code::solve!(DAY, 1, part_one, input);
advent_of_code::solve!(DAY, 2, part_two, input);
}
advent_of_code::main!(DAY);
#[cfg(test)]
mod tests {
@ -28,23 +20,18 @@ mod tests {
#[test]
fn test_part_one() {
let input = advent_of_code::read_file("examples", DAY);
assert_eq!(part_one(&input), None);
let result = part_one(&advent_of_code::template::read_file("examples", DAY));
assert_eq!(result, None);
}
#[test]
fn test_part_two() {
let input = advent_of_code::read_file("examples", DAY);
assert_eq!(part_two(&input), None);
let result = part_two(&advent_of_code::template::read_file("examples", DAY));
assert_eq!(result, None);
}
}
"#;
fn parse_args() -> Result<u8, pico_args::Error> {
let mut args = pico_args::Arguments::from_env();
args.free_from_str()
}
fn safe_create_file(path: &str) -> Result<File, std::io::Error> {
OpenOptions::new().write(true).create_new(true).open(path)
}
@ -53,25 +40,17 @@ fn create_file(path: &str) -> Result<File, std::io::Error> {
OpenOptions::new().write(true).create(true).open(path)
}
fn main() {
let day = match parse_args() {
Ok(day) => day,
Err(_) => {
eprintln!("Need to specify a day (as integer). example: `cargo scaffold 7`");
process::exit(1);
}
};
pub fn scaffold_handler(day: u8) {
let day_padded = format!("{:02}", day);
let day_padded = format!("{day:02}");
let input_path = format!("src/inputs/{day_padded}.txt");
let example_path = format!("src/examples/{day_padded}.txt");
let module_path = format!("src/bin/{day_padded}.rs");
let input_path = format!("data/inputs/{}.txt", day_padded);
let example_path = format!("data/examples/{}.txt", day_padded);
let module_path = format!("src/bin/{}.rs", day_padded);
let mut file = match safe_create_file(&module_path) {
Ok(file) => file,
Err(e) => {
eprintln!("Failed to create module file: {e}");
eprintln!("Failed to create module file: {}", e);
process::exit(1);
}
};
@ -81,7 +60,7 @@ fn main() {
println!("Created module file \"{}\"", &module_path);
}
Err(e) => {
eprintln!("Failed to write module contents: {e}");
eprintln!("Failed to write module contents: {}", e);
process::exit(1);
}
}
@ -91,7 +70,7 @@ fn main() {
println!("Created empty input file \"{}\"", &input_path);
}
Err(e) => {
eprintln!("Failed to create input file: {e}");
eprintln!("Failed to create input file: {}", e);
process::exit(1);
}
}
@ -101,7 +80,7 @@ fn main() {
println!("Created empty example file \"{}\"", &example_path);
}
Err(e) => {
eprintln!("Failed to create example file: {e}");
eprintln!("Failed to create example file: {}", e);
process::exit(1);
}
}

View file

@ -0,0 +1,31 @@
use std::process::{Command, Stdio};
pub fn solve_handler(day: u8, release: bool, time: bool, submit_part: Option<u8>) {
let day_padded = format!("{:02}", day);
let mut cmd_args = vec!["run".to_string(), "--bin".to_string(), day_padded];
if release {
cmd_args.push("--release".to_string());
}
cmd_args.push("--".to_string());
if let Some(submit_part) = submit_part {
cmd_args.push("--submit".to_string());
cmd_args.push(submit_part.to_string())
}
if time {
cmd_args.push("--time".to_string());
}
let mut cmd = Command::new("cargo")
.args(&cmd_args)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.unwrap();
cmd.wait().unwrap();
}

34
src/template/mod.rs Normal file
View file

@ -0,0 +1,34 @@
use std::{env, fs};
pub mod aoc_cli;
pub mod commands;
pub mod readme_benchmarks;
pub mod runner;
pub const ANSI_ITALIC: &str = "\x1b[3m";
pub const ANSI_BOLD: &str = "\x1b[1m";
pub const ANSI_RESET: &str = "\x1b[0m";
/// Helper function that reads a text file to a string.
pub fn read_file(folder: &str, day: u8) -> String {
let cwd = env::current_dir().unwrap();
let filepath = cwd
.join("data")
.join(folder)
.join(format!("{:02}.txt", day));
let f = fs::read_to_string(filepath);
f.expect("could not open input file")
}
/// main! produces a block setting up the input and runner for each part.
#[macro_export]
macro_rules! main {
($day:expr) => {
fn main() {
use advent_of_code::template::runner::*;
let input = advent_of_code::template::read_file("inputs", $day);
run_part(part_one,&input, $day, 1);
run_part(part_two, &input, $day, 2);
}
};
}

View file

@ -0,0 +1,183 @@
/// Module that updates the readme me with timing information.
/// The approach taken is similar to how `aoc-readme-stars` handles this.
use std::{fs, io};
static MARKER: &str = "<!--- benchmarking table --->";
#[derive(Debug)]
pub enum Error {
Parser(String),
IO(io::Error),
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::IO(e)
}
}
#[derive(Clone)]
pub struct Timings {
pub day: usize,
pub part_1: Option<String>,
pub part_2: Option<String>,
pub total_nanos: f64,
}
pub struct TablePosition {
pos_start: usize,
pos_end: usize,
}
pub fn get_path_for_bin(day: usize) -> String {
let day_padded = format!("{:02}", day);
format!("./src/bin/{}.rs", day_padded)
}
fn locate_table(readme: &str) -> Result<TablePosition, Error> {
let matches: Vec<_> = readme.match_indices(MARKER).collect();
if matches.len() > 2 {
return Err(Error::Parser(
"{}: too many occurences of marker in README.".into(),
));
}
let pos_start = matches
.first()
.map(|m| m.0)
.ok_or_else(|| Error::Parser("Could not find table start position.".into()))?;
let pos_end = matches
.last()
.map(|m| m.0 + m.1.len())
.ok_or_else(|| Error::Parser("Could not find table end position.".into()))?;
Ok(TablePosition { pos_start, pos_end })
}
fn construct_table(prefix: &str, timings: Vec<Timings>, total_millis: f64) -> String {
let header = format!("{prefix} Benchmarks");
let mut lines: Vec<String> = vec![
MARKER.into(),
header,
"".into(),
"| Day | Part 1 | Part 2 |".into(),
"| :---: | :---: | :---: |".into(),
];
timings.into_iter().for_each(|timing| {
let path = get_path_for_bin(timing.day);
lines.push(format!(
"| [Day {}]({}) | `{}` | `{}` |",
timing.day,
path,
timing.part_1.unwrap_or_else(|| "-".into()),
timing.part_2.unwrap_or_else(|| "-".into())
));
});
lines.push("".into());
lines.push(format!("**Total: {:.2}ms**", total_millis));
lines.push(MARKER.into());
lines.join("\n")
}
fn update_content(s: &mut String, timings: Vec<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<Timings>, total_millis: f64) -> Result<(), Error> {
let path = "README.md";
let mut readme = String::from_utf8_lossy(&fs::read(path)?).to_string();
update_content(&mut readme, timings, total_millis)?;
fs::write(path, &readme)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::{update_content, Timings, MARKER};
fn get_mock_timings() -> Vec<Timings> {
vec![
Timings {
day: 1,
part_1: Some("10ms".into()),
part_2: Some("20ms".into()),
total_nanos: 3e+10,
},
Timings {
day: 2,
part_1: Some("30ms".into()),
part_2: Some("40ms".into()),
total_nanos: 7e+10,
},
Timings {
day: 4,
part_1: Some("40ms".into()),
part_2: Some("50ms".into()),
total_nanos: 9e+10,
},
]
}
#[test]
#[should_panic]
fn errors_if_marker_not_present() {
let mut s = "# readme".to_string();
update_content(&mut s, get_mock_timings(), 190.0).unwrap();
}
#[test]
#[should_panic]
fn errors_if_too_many_markers_present() {
let mut s = format!("{} {} {}", MARKER, MARKER, MARKER);
update_content(&mut s, get_mock_timings(), 190.0).unwrap();
}
#[test]
fn updates_empty_benchmarks() {
let mut s = format!("foo\nbar\n{}{}\nbaz", MARKER, MARKER);
update_content(&mut s, get_mock_timings(), 190.0).unwrap();
assert_eq!(s.contains("## Benchmarks"), true);
}
#[test]
fn updates_existing_benchmarks() {
let mut s = format!("foo\nbar\n{}{}\nbaz", MARKER, MARKER);
update_content(&mut s, get_mock_timings(), 190.0).unwrap();
update_content(&mut s, get_mock_timings(), 190.0).unwrap();
assert_eq!(s.matches(MARKER).collect::<Vec<&str>>().len(), 2);
assert_eq!(s.matches("## Benchmarks").collect::<Vec<&str>>().len(), 1);
}
#[test]
fn format_benchmarks() {
let mut s = format!("foo\nbar\n{}\n{}\nbaz", MARKER, MARKER);
update_content(&mut s, get_mock_timings(), 190.0).unwrap();
let expected = [
"foo",
"bar",
"<!--- benchmarking table --->",
"## Benchmarks",
"",
"| Day | Parser | Part 1 | Part 2 |",
"| :---: | :---: | :---: | :---: |",
"| [Day 1](./src/bin/01.rs) | `-` | `10ms` | `20ms` |",
"| [Day 2](./src/bin/02.rs) | `-` | `30ms` | `40ms` |",
"| [Day 4](./src/bin/04.rs) | `-` | `40ms` | `50ms` |",
"",
"**Total: 190.00ms**",
"<!--- benchmarking table --->",
"baz",
]
.join("\n");
assert_eq!(s, expected);
}
}

165
src/template/runner.rs Normal file
View file

@ -0,0 +1,165 @@
/// Encapsulates code that interacts with solution functions.
use crate::template::{aoc_cli, ANSI_ITALIC, ANSI_RESET};
use std::fmt::Display;
use std::io::{stdout, Write};
use std::process::Output;
use std::time::{Duration, Instant};
use std::{cmp, env, process};
use super::ANSI_BOLD;
pub fn run_part<I: Clone, T: Display>(func: impl Fn(I) -> Option<T>, input: I, day: u8, part: u8) {
let part_str = format!("Part {}", part);
let (result, duration, samples) =
run_timed(func, input, |result| print_result(result, &part_str, ""));
print_result(&result, &part_str, &format_duration(&duration, samples));
if let Some(result) = result {
submit_result(result, day, part);
}
}
/// Run a solution part. The behavior differs depending on whether we are running a release or debug build:
/// 1. in debug, the function is executed once.
/// 2. in release, the function is benched (approx. 1 second of execution time or 10 samples, whatever take longer.)
fn run_timed<I: Clone, T>(
func: impl Fn(I) -> T,
input: I,
hook: impl Fn(&T),
) -> (T, Duration, u128) {
let timer = Instant::now();
let result = func(input.clone());
let base_time = timer.elapsed();
hook(&result);
let run = match std::env::args().any(|x| x == "--time") {
true => bench(func, input, &base_time),
false => (base_time, 1),
};
(result, run.0, run.1)
}
fn bench<I: Clone, T>(func: impl Fn(I) -> T, input: I, base_time: &Duration) -> (Duration, u128) {
let mut stdout = stdout();
print!(" > {}benching{}", ANSI_ITALIC, ANSI_RESET);
let _ = stdout.flush();
let bench_iterations = cmp::min(
100000,
cmp::max(
Duration::from_secs(1).as_nanos() / cmp::max(base_time.as_nanos(), 10),
10,
),
);
let mut timers: Vec<Duration> = vec![];
for _ in 0..bench_iterations {
// need a clone here to make the borrow checker happy.
let cloned = input.clone();
let timer = Instant::now();
func(cloned);
timers.push(timer.elapsed());
}
(
Duration::from_nanos(average_duration(&timers) as u64),
bench_iterations as u128,
)
}
fn average_duration(numbers: &[Duration]) -> u128 {
numbers.iter().map(|d| d.as_nanos()).sum::<u128>() / numbers.len() as u128
}
fn format_duration(duration: &Duration, samples: u128) -> String {
if samples == 1 {
format!(" ({:.1?})", duration)
} else {
format!(" ({:.1?} @ {} samples)", duration, samples)
}
}
fn print_result<T: Display>(result: &Option<T>, part: &str, duration_str: &str) {
let is_intermediate_result = duration_str.is_empty();
match result {
Some(result) => {
if result.to_string().contains('\n') {
let str = format!("{}: ▼ {}", part, duration_str);
if is_intermediate_result {
print!("{}", str);
} else {
print!("\r");
println!("{}", str);
println!("{}", result);
}
} else {
let str = format!(
"{}: {}{}{}{}",
part, ANSI_BOLD, result, ANSI_RESET, duration_str
);
if is_intermediate_result {
print!("{}", str);
} else {
print!("\r");
println!("{}", str);
}
}
}
None => {
if is_intermediate_result {
print!("{}: ✖", part);
} else {
print!("\r");
println!("{}: ✖ ", part);
}
}
}
}
/// Parse the arguments passed to `solve` and try to submit one part of the solution if:
/// 1. we are in `--release` mode.
/// 2. aoc-cli is installed.
fn submit_result<T: Display>(
result: T,
day: u8,
part: u8,
) -> Option<Result<Output, aoc_cli::AocCliError>> {
let args: Vec<String> = env::args().collect();
if !args.contains(&"--submit".into()) {
return None;
}
if args.len() < 3 {
eprintln!("Unexpected command-line input. Format: cargo solve 1 --submit 1");
process::exit(1);
}
let part_index = args.iter().position(|x| x == "--submit").unwrap() + 1;
let part_submit = match args[part_index].parse::<u8>() {
Ok(x) => x,
Err(_) => {
eprintln!("Unexpected command-line input. Format: cargo solve 1 --submit 1");
process::exit(1);
}
};
if part_submit != part {
return None;
}
if aoc_cli::check().is_err() {
eprintln!("command \"aoc\" not found or not callable. Try running \"cargo install aoc-cli\" to install it.");
process::exit(1);
}
println!("Submitting result via aoc-cli...");
Some(aoc_cli::submit(day, part, &result.to_string()))
}