1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
//! User-facing output.
//!
//! Data goes to standard output so `cd $(aoc path)` works; diagnostics go to
//! standard error. Handlers emit semantic [`Event`]s, so tests assert on what
//! happened rather than on ANSI escape sequences.
use crate::{aoc::Verdict, puzzle::Part};
use colored::Colorize as _;
use std::io::{self, Write as _};
/// Something worth telling the user about.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Event {
/// A machine-consumable result, such as a path or a URL.
Data(String),
/// A solution's own output, passed through verbatim.
Raw(String),
/// An answer and how the site judged it.
Answer {
/// Which part the answer belongs to.
part: Part,
/// The answer itself.
answer: String,
/// The site's verdict, or `None` when nothing was submitted.
verdict: Option<Verdict>,
},
/// A non-fatal problem.
Warning(String),
}
/// Receives [`Event`]s and presents them.
pub trait Reporter {
/// Presents a single event.
fn report(&mut self, event: Event);
/// Emits a machine-consumable result.
fn data(&mut self, text: &str) {
self.report(Event::Data(text.to_owned()));
}
/// Emits a solution's own output.
fn raw(&mut self, text: &str) {
self.report(Event::Raw(text.to_owned()));
}
/// Emits an answer and its verdict.
fn answer(&mut self, part: Part, answer: &str, verdict: Option<Verdict>) {
self.report(Event::Answer {
part,
answer: answer.to_owned(),
verdict,
});
}
/// Emits a non-fatal problem.
fn warn(&mut self, text: &str) {
self.report(Event::Warning(text.to_owned()));
}
}
/// Writes events to the terminal, colouring answers by verdict.
#[derive(Debug, Default, Clone, Copy)]
pub struct TermReporter;
impl Reporter for TermReporter {
fn report(&mut self, event: Event) {
match event {
Event::Data(text) => println!("{text}"),
Event::Raw(text) => {
print!("{text}");
if !text.ends_with('\n') {
println!();
}
let _ = io::stdout().flush();
}
Event::Answer {
part,
answer,
verdict,
} => match &verdict {
None => println!("{answer}"),
Some(Verdict::Correct) => println!("{}", answer.green()),
Some(verdict) => {
// Red is an answer the site judged and rejected; yellow is
// one it declined to judge, which says nothing about the
// answer itself.
let coloured = if verdict.is_judged() {
answer.red()
} else {
answer.yellow()
};
println!("{coloured}");
eprintln!("{part}: {verdict}");
}
},
Event::Warning(text) => eprintln!("{} {text}", "warning:".yellow().bold()),
}
}
}
#[cfg(test)]
pub(crate) mod recording {
use super::{Event, Reporter};
#[derive(Debug, Default)]
pub(crate) struct RecordingReporter {
pub(crate) events: Vec<Event>,
}
impl RecordingReporter {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn raw_output(&self) -> String {
self.events
.iter()
.filter_map(|event| match event {
Event::Raw(text) => Some(text.as_str()),
_ => None,
})
.collect()
}
pub(crate) fn answers(&self) -> Vec<Event> {
self.events
.iter()
.filter(|event| matches!(event, Event::Answer { .. }))
.cloned()
.collect()
}
pub(crate) fn warnings(&self) -> Vec<&str> {
self.events
.iter()
.filter_map(|event| match event {
Event::Warning(text) => Some(text.as_str()),
_ => None,
})
.collect()
}
}
impl Reporter for RecordingReporter {
fn report(&mut self, event: Event) {
self.events.push(event);
}
}
}
#[cfg(test)]
mod tests {
use super::{recording::RecordingReporter, *};
#[test]
fn records_every_kind_of_event() {
let mut reporter = RecordingReporter::new();
reporter.data("/aoc/2024/day07/rust");
reporter.raw("hello\n");
reporter.answer(Part::One, "1227", Some(Verdict::Correct));
reporter.warn("no cookie");
assert_eq!(reporter.events.len(), 4);
assert_eq!(reporter.raw_output(), "hello\n");
assert_eq!(reporter.warnings(), ["no cookie"]);
assert_eq!(
reporter.events[0],
Event::Data("/aoc/2024/day07/rust".to_owned())
);
assert_eq!(
reporter.events[2],
Event::Answer {
part: Part::One,
answer: "1227".to_owned(),
verdict: Some(Verdict::Correct),
}
);
}
}