Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Assert #58

Merged
merged 5 commits into from
Dec 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions src/assert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use crate::{
error::AocError,
util::{get_day_title_and_answers, parse_get_answers, Task},
};

fn assert_print_equal(expected: &str, actual: &str, task: Task)
{
if expected == actual
{
println!("Task {}: \x1b[0;32mok\x1b[0m", task);
}
else
{
println!(
"Task {}: \x1b[0;31mFAILED\x1b[0m
expected: `{}`,
actual: `{}`",
task, expected, actual
)
}
}

fn assert_print_fail(s: &str, task: Task)
{
println!(
"Task {}: \x1b[0;31mFAILED\x1b[0m
`{}`",
task, s
)
}

pub async fn assert_answer(out: &str, day: u32, year: i32) -> Result<(), AocError>
{
let info = get_day_title_and_answers(day, year as u32).await?;
let (p1, p2) = parse_get_answers(out);

match (p1, p2, info.part1_answer, info.part2_answer)
{
(Some(p1), Some(p2), Some(a1), Some(a2)) =>
{
assert_print_equal(&a1, &p1, Task::One);
assert_print_equal(&a2, &p2, Task::Two);
},
(Some(p1), None, Some(a1), Some(a2)) =>
{
assert_print_equal(&a1, &p1, Task::One);
assert_print_fail(
&format!("Couldn't verify answer against the correct one: {}", a2),
Task::Two,
);
},
(None, Some(p2), Some(a1), Some(a2)) =>
{
assert_print_fail(
&format!("Couldn't verify answer against the correct one: {}", a1),
Task::One,
);
assert_print_equal(&a2, &p2, Task::Two);
},
(Some(p1), _, Some(a1), None) =>
{
assert_print_equal(&a1, &p1, Task::One);
assert_print_fail("Have you completed it?", Task::Two);
},
(None, Some(_), Some(a1), None) =>
{
assert_print_fail(
&format!("Couldn't verify answer against the correct one: {}", a1),
Task::One,
);
assert_print_fail("Coulnd't find the submitted answer", Task::Two);
},
(None, None, _, _) =>
{
assert_print_fail("Have you completed it?", Task::One);
assert_print_fail("Have you completed it?", Task::Two);
},
// Assumes that it is impossible to get answer for part 2 if we don't get answer for part 1
(_, _, None, _) =>
{
assert_print_fail("Coulnd't find the submitted answer", Task::One);
assert_print_fail("Coulnd't find the submitted answer", Task::Two);
},
}

Ok(())
}
9 changes: 8 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use chrono::Datelike;
use clap::{builder::OsStr, Arg, Command};
use error::AocError;
mod assert;
#[cfg(feature = "bench")] mod bench;
mod clippy;
mod error;
Expand All @@ -15,7 +16,7 @@ async fn main() -> Result<(), AocError>
{
dotenv::dotenv().ok();
let mut cmd = Command::new("cargo-aoc")
.author("Sebastian, seblyng98@gmail.com")
.author("Sebastian, sebastian@lyngjohansen.com")
.author("Sivert, [email protected]")
.arg(Arg::new("dummy").hide(true))
.subcommand(
Expand Down Expand Up @@ -68,6 +69,12 @@ async fn main() -> Result<(), AocError>
.required(false)
.action(clap::ArgAction::SetTrue)
.help("Run the day with the \"test\" file"),
Arg::new("assert")
.short('a')
.long("assert")
.required(false)
.action(clap::ArgAction::SetTrue)
.help("Asserts that the answers are still correct after submitting"),
Arg::new("compiler-flags")
.short('C')
.long("compiler-flags")
Expand Down
7 changes: 7 additions & 0 deletions src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use duct::cmd;

#[cfg(feature = "submit")] use crate::util::submit::{self, get_submit_task};
use crate::{
assert::assert_answer,
error::AocError,
util::{
file::{cargo_path, day_path, download_input_file},
Expand Down Expand Up @@ -81,6 +82,12 @@ pub async fn run(matches: &ArgMatches) -> Result<(), AocError>
}
}

if matches.get_flag("assert")
{
let year = get_year(matches)?;
assert_answer(&out, day, year).await?;
}

// Only try to submit if the submit flag is passed
#[cfg(feature = "submit")]
if let Some(task) = get_submit_task(matches).transpose()?
Expand Down
121 changes: 121 additions & 0 deletions src/util/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,37 @@
use std::path::PathBuf;

use clap::ArgMatches;

use self::{
file::{cargo_path, day_path},
request::AocRequest,
};
use crate::error::AocError;

pub mod file;
pub mod request;
#[cfg(feature = "submit")] pub mod submit;

#[derive(Eq, PartialEq, Clone, Copy)]
pub enum Task
{
One,
Two,
}

impl std::fmt::Display for Task
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
{
match self
{
Task::One => write!(f, "one"),
Task::Two => write!(f, "two"),
}
}
}


pub fn get_year(matches: &ArgMatches) -> Result<i32, AocError>
{
let year = matches.get_one::<String>("year").ok_or(AocError::ArgMatches)?;
Expand Down Expand Up @@ -44,3 +70,98 @@ pub fn get_time_symbol() -> String
sym
}
}

#[derive(Debug)]
pub struct AocInfo
{
pub day: u32,
pub year: u32,
pub title: String,
pub part1_answer: Option<String>,
pub part2_answer: Option<String>,
}

pub async fn get_day_title_and_answers(day: u32, year: u32) -> Result<AocInfo, AocError>
{
if let Ok(cache) = read_cache_answers(day, year).await
{
return Ok(cache);
}

let url = format!("https://adventofcode.com/{}/day/{}", year, day);

let res = AocRequest::new().get(&url).await?;

let text = res.text().await?;

let h2 = "<h2>--- ";
let idx1 = text.find(h2).unwrap() + h2.len();
let idx2 = text[idx1..].find(" ---</h2>").unwrap();
let (_, title) = text[idx1..idx1 + idx2].split_once(": ").unwrap();

let search = "Your puzzle answer was <code>";
let mut iter = text.lines().filter(|&line| line.contains(search)).map(|line| {
let code_end = "</code>";
let idx = line.find(search).unwrap() + search.len();
let end = line[idx..].find(code_end).unwrap();

line[idx..idx + end].to_owned()
});
let a1 = iter.next();
let a2 = iter.next();

let info = AocInfo {
day,
year,
title: title.to_owned(),
part1_answer: a1,
part2_answer: a2,
};

// Ignore possible errors during cache write
let _ = write_cache_answers(day, &info).await;

Ok(info)
}

pub fn parse_get_answers(output: &str) -> (Option<String>, Option<String>)
{
let strip = strip_ansi_escapes::strip(output);
let text = std::str::from_utf8(&strip).unwrap();

let parse = |line: &str| line.split_ascii_whitespace().next_back().map(|s| s.to_string());
let mut iter = text.split('\n');
(iter.next().and_then(parse), iter.next().and_then(parse))
}

async fn get_cache_path(day: u32) -> Result<PathBuf, AocError>
{
// Tries to read it from the cache before making a request
let path = cargo_path().await?;
Ok(day_path(path, day).await?.join(".answers"))
}

pub async fn write_cache_answers(day: u32, info: &AocInfo) -> Result<(), AocError>
{
let path = get_cache_path(day).await?;
if let (Some(a1), Some(a2)) = (&info.part1_answer, &info.part2_answer)
{
tokio::fs::write(path, format!("{}\n{}\n{}", info.title, a1, a2)).await?;
}

Ok(())
}

pub async fn read_cache_answers(day: u32, year: u32) -> Result<AocInfo, AocError>
{
let path = get_cache_path(day).await?;
let res = tokio::fs::read_to_string(path).await?;
let lines = res.lines().collect::<Vec<_>>();
Ok(AocInfo {
day,
year,
title: lines[0].to_owned(),
part1_answer: Some(lines[1].to_owned()),
part2_answer: Some(lines[2].to_owned()),
})
}
3 changes: 2 additions & 1 deletion src/util/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ pub struct AocRequest

impl AocRequest
{
const AOC_USER_AGENT: &str = "github.com/seblj/cargo-aoc by [email protected]";
const AOC_USER_AGENT: &str =
"github.com/seblj/cargo-aoc by [email protected] and [email protected]";

pub fn new() -> AocRequest
{
Expand Down
32 changes: 3 additions & 29 deletions src/util/submit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::collections::HashMap;
use clap::ArgMatches;
use sanitize_html::rules::predefined::DEFAULT;

use super::request::AocRequest;
use super::{parse_get_answers, request::AocRequest, Task};
use crate::error::AocError;

pub fn get_submit_task(matches: &ArgMatches) -> Option<Result<Task, AocError>>
Expand All @@ -21,33 +21,6 @@ pub fn get_submit_task(matches: &ArgMatches) -> Option<Result<Task, AocError>>
}
}

#[derive(Eq, PartialEq, Clone, Copy)]
pub enum Task
{
One,
Two,
}

impl std::fmt::Display for Task
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
{
match self
{
Task::One => write!(f, "one"),
Task::Two => write!(f, "two"),
}
}
}

fn get_answer(out: &str, task: Task) -> Option<String>
{
let start = out.split(&format!("Task {}: ", task)).nth(1)?;
let encoded_answer = start.split_once('\n').unwrap_or((start, "")).0;
let answer = strip_ansi_escapes::strip(encoded_answer);
String::from_utf8(answer).ok()
}

fn parse_and_sanitize_output(output: &str) -> Option<String>
{
let start = output.find("<article><p>")?;
Expand All @@ -58,7 +31,8 @@ fn parse_and_sanitize_output(output: &str) -> Option<String>

pub async fn submit(output: &str, task: Task, day: u32, year: i32) -> Result<String, AocError>
{
let answer = get_answer(output, task).ok_or(AocError::ParseStdout)?;
let (p1, p2) = parse_get_answers(output);
let answer = if task == Task::One { p1 } else { p2 }.ok_or(AocError::ParseStdout)?;
let url = format!("https://adventofcode.com/{}/day/{}/answer", year, day);

let mut form = HashMap::new();
Expand Down