Skip to content

Commit

Permalink
scaffold: Submit command
Browse files Browse the repository at this point in the history
  • Loading branch information
connorslade committed Nov 29, 2023
1 parent 723f2ca commit b4f7bfe
Show file tree
Hide file tree
Showing 11 changed files with 202 additions and 17 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ mod misc;
mod solution;

pub use answer::Answer;
pub use misc::load;
pub use misc::{human_time, load};
pub use solution::{DummySolution, Solution};
14 changes: 14 additions & 0 deletions common/src/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,17 @@ pub fn load_raw(year: u32, day: u32) -> io::Result<String> {
let file = format!("data/{year}/{:02}.txt", day);
fs::read_to_string(file)
}

pub fn human_time(time: u128) -> String {
const TIME_UNITS: &[&str] = &["ns", "μs", "ms", "s"];

let mut time = time;
for i in TIME_UNITS {
if time < 1000 {
return format!("{}{}", time, i);
}
time /= 1000;
}

format!("{}{}", time, TIME_UNITS.last().unwrap())
}
1 change: 1 addition & 0 deletions scaffold/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ anyhow = "1.0.75"
chrono = "0.4.31"
clap = { version = "4.4.8", features = ["derive"] }
colored = "2.0.4"
common = { path = "../common" }
globalenv = "0.4.2"
humantime = "2.1.0"
once_cell = "1.18.0"
Expand Down
57 changes: 57 additions & 0 deletions scaffold/src/args.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use clap::Parser;
use regex::Regex;
use url::Url;

use crate::misc::current_year;
Expand Down Expand Up @@ -28,6 +29,8 @@ pub enum SubCommand {
/// Fetch the puzzle input for a given day and write to a file.
/// Also creates a base solution file for the given day.
Init(InitArgs),
/// Submit a solution to the Advent of Code server.
Submit(SubmitArgs),
}

#[derive(Parser, Debug)]
Expand Down Expand Up @@ -91,3 +94,57 @@ pub struct InitArgs {
#[arg(default_value_t = current_year())]
pub year: u16,
}

#[derive(Parser, Debug)]
pub struct SubmitArgs {
/// Command to run to get the solution for the given day.
#[arg(
short,
long,
default_value = "cargo r -r -- run {{day}} {{part}} {{year}}"
)]
pub command: String,
/// A regex that will be used to extract the solution from the output of the command.
#[arg(long, default_value = r"OUT: (.*) \(")]
pub extraction_regex: Regex,
/// The group of the regex that contains the solution.
#[arg(long, default_value = "1")]
pub extraction_group: usize,
/// Don't actually submit the solution.
/// Useful for testing that the command and extraction regex are correct.
#[arg(short, long)]
pub dry_run: bool,

/// The day to submit the solution for.
pub day: u8,
/// The part to submit the solution for.
#[arg(value_parser = parse_part)]
pub part: Part,
/// The year to submit the solution for.
#[arg(default_value_t = current_year())]
pub year: u16,
}

#[derive(Debug, Clone, Copy)]
pub enum Part {
A,
B,
}

fn parse_part(s: &str) -> Result<Part, String> {
match s {
"a" => Ok(Part::A),
"b" => Ok(Part::B),
_ => Err("part must be `a` or `b`".to_owned()),
}
}

impl ToString for Part {
fn to_string(&self) -> String {
match self {
Part::A => "a",
Part::B => "b",
}
.to_owned()
}
}
1 change: 1 addition & 0 deletions scaffold/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pub mod init;
pub mod timer;
pub mod token;
pub mod verify;
pub mod submit;
103 changes: 103 additions & 0 deletions scaffold/src/commands/submit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use std::{
process::{Command, Stdio},
time::Instant,
};

use anyhow::{Context, Result};
use common::human_time;
use scraper::Html;

use crate::{
args::{Args, SubmitArgs},
formatter::Formatter,
session::{Authenticated, Session},
};

pub fn submit(session: &Session, cmd: &SubmitArgs, args: &Args) -> Result<()> {
let answer = get_answer(cmd).context("Getting answer")?;

if cmd.dry_run {
println!("[*] Aborting due to dry run");
return Ok(());
}

submit_answer(session, cmd, args, &answer).context("Submitting answer")?;
Ok(())
}

fn get_answer(cmd: &SubmitArgs) -> Result<String> {
let formats: &[(&str, String)] = &[
("day", cmd.day.to_string()),
("year", cmd.year.to_string()),
("part", cmd.part.to_string()),
];
let command = Formatter::new(&cmd.command)?.format(formats)?;
let args = shell_words::split(&command)?;
let executable = which::which(&args[0])?;

if cmd.dry_run {
println!("[*] Running command: {}", command);
}

let executable = Command::new(&executable)
.args(&args[1..])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;

let start = Instant::now();
let output = executable.wait_with_output()?;
let time = start.elapsed().as_nanos();

if output.status.code() != Some(0) {
anyhow::bail!(
"Command failed with status code {}\n{}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
}

let output = String::from_utf8_lossy(&output.stdout);
let answer = cmd
.extraction_regex
.captures(&output)
.context("Failed to extract answer, regex didn't match")?
.get(cmd.extraction_group)
.context("Failed to extract answer, too few capture groups")?
.as_str()
.trim()
.to_owned();

println!("[*] Answer: `{answer}` ({})", human_time(time));

Ok(answer)
}

fn submit_answer(session: &Session, cmd: &SubmitArgs, args: &Args, answer: &str) -> Result<()> {
let url = args
.address
.join(&format!("{}/day/{}/answer", cmd.year, cmd.day))?;

// POST https://adventofcode.com/{{year}}/day/{{day}}/answer
// level={{part:int}}&answer={{answer}}
let result = ureq::post(url.as_str())
.authenticated(session)
.send_form(&[
("level", &(cmd.part as u8 + 1).to_string()),
("answer", answer),
])?;

let document = Html::parse_document(&result.into_string()?);
let result = document
.select(selector!("article p"))
.next()
.context("No response message found")?;
let result_text = result.text().collect::<Vec<_>>().join("");

// Remove duplicate whitespace
let result_text = regex!(r"[\[\(].*?[\]\)]").replace_all(&result_text, "");
let result_text = regex!(r"\s+").replace_all(&result_text, " ");

println!("[*] {result_text}");
Ok(())
}
1 change: 1 addition & 0 deletions scaffold/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ fn main() -> Result<()> {
SubCommand::Token(e) => commands::token::token(&session.ok(), e, &args)?,
SubCommand::Timer(e) => commands::timer::timer(e)?,
SubCommand::Init(e) => commands::init::init(&session?, e, &args)?,
SubCommand::Submit(e) => commands::submit::submit(&session?, e, &args)?,
}

Ok(())
Expand Down
21 changes: 21 additions & 0 deletions scaffold/template.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,24 @@ impl Solution for Day{{day:pad(2)}} {
Answer::Unimplemented
}
}

#[cfg(test)]
mod test {
use indoc::indoc;

use super::Day{{day:pad(2)}};

const CASE: &str = indoc! {"
...
"};

#[test]
fn test_a() {
assert_eq!(Day{{day:pad(2)}}.part_a(CASE), Answer::Unimplemented);
}

#[test]
fn test_b() {
assert_eq!(Day{{day:pad(2)}}.part_b(CASE), Answer::Unimplemented);
}
}
2 changes: 1 addition & 1 deletion scaffold/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
- [x] Verify
- [x] Token
- [x] Timer
- [ ] Allow not filling the solution array
- [x] Submit
16 changes: 1 addition & 15 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::time::Instant;

use clap::Parser;
use common::Solution;
use common::{human_time, Solution};

use args::{Args, Commands};
mod args;
Expand Down Expand Up @@ -67,17 +67,3 @@ fn get_year(year: u32) -> &'static [&'static dyn Solution] {
_ => &[],
}
}

pub fn human_time(time: u128) -> String {
const TIME_UNITS: &[&str] = &["ns", "μs", "ms", "s"];

let mut time = time;
for i in TIME_UNITS {
if time < 1000 {
return format!("{}{}", time, i);
}
time /= 1000;
}

format!("{}{}", time, TIME_UNITS.last().unwrap())
}

0 comments on commit b4f7bfe

Please sign in to comment.