Skip to content

Commit c7d14cb

Browse files
committed
Use a manual parser for rust-timer queue
Instead of a regex. This allows the user to pass the arguments in an arbitrary order.
1 parent 025f64b commit c7d14cb

File tree

3 files changed

+176
-18
lines changed

3 files changed

+176
-18
lines changed

Cargo.lock

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,6 @@ jemalloc-ctl = "0.5"
5757
serde = { workspace = true, features = ["derive"] }
5858
serde_json = { workspace = true }
5959
toml = "0.7"
60+
61+
[dev-dependencies]
62+
insta = "1.40.0"

site/src/request_handlers/github.rs

Lines changed: 154 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,13 @@ use crate::github::{
55
};
66
use crate::load::SiteCtxt;
77

8-
use std::sync::Arc;
9-
8+
use hashbrown::HashMap;
109
use regex::Regex;
10+
use std::sync::Arc;
1111

1212
lazy_static::lazy_static! {
1313
static ref BODY_TIMER_BUILD: Regex =
1414
Regex::new(r"(?:\W|^)@rust-timer\s+build\s+(\w+)(?:\W|$)(?:include=(\S+))?\s*(?:exclude=(\S+))?\s*(?:runs=(\d+))?").unwrap();
15-
static ref BODY_TIMER_QUEUE: Regex =
16-
Regex::new(r"(?:\W|^)@rust-timer\s+queue(?:\W|$)(?:include=(\S+))?\s*(?:exclude=(\S+))?\s*(?:runs=(\d+))?").unwrap();
1715
}
1816

1917
pub async fn handle_github(
@@ -118,26 +116,25 @@ async fn handle_rust_timer(
118116
return Ok(github::Response);
119117
}
120118

121-
if let Some(captures) = BODY_TIMER_QUEUE.captures(&comment.body) {
122-
let include = captures.get(1).map(|v| v.as_str());
123-
let exclude = captures.get(2).map(|v| v.as_str());
124-
let runs = captures.get(3).and_then(|v| v.as_str().parse::<i32>().ok());
125-
{
126-
let conn = ctxt.conn().await;
127-
conn.queue_pr(issue.number, include, exclude, runs).await;
128-
}
129-
main_client
130-
.post_comment(
131-
issue.number,
119+
if let Some(queue) = parse_queue_command(&comment.body) {
120+
let msg = match queue {
121+
Ok(cmd) => {
122+
let conn = ctxt.conn().await;
123+
conn.queue_pr(issue.number, cmd.include, cmd.exclude, cmd.runs)
124+
.await;
132125
format!(
133126
"Awaiting bors try build completion.
134127
135128
@rustbot label: +S-waiting-on-perf
136129
137130
{COMMENT_MARK_TEMPORARY}"
138-
),
139-
)
140-
.await;
131+
)
132+
}
133+
Err(error) => {
134+
format!("Error occurred while parsing comment: {error}")
135+
}
136+
};
137+
main_client.post_comment(issue.number, msg).await;
141138
return Ok(github::Response);
142139
}
143140

@@ -163,6 +160,68 @@ async fn handle_rust_timer(
163160
Ok(github::Response)
164161
}
165162

163+
/// Parses the first occurrence of a `@rust-timer queue <...>` command
164+
/// in the input string.
165+
fn parse_queue_command(body: &str) -> Option<Result<QueueCommand, String>> {
166+
let prefix = "@rust-timer";
167+
let bot_line = body.lines().find_map(|line| {
168+
let line = line.trim();
169+
line.find(prefix)
170+
.map(|index| line[index + prefix.len()..].trim())
171+
})?;
172+
173+
let args = bot_line.strip_prefix("queue").map(|l| l.trim())?;
174+
let mut args = match parse_command_arguments(args) {
175+
Ok(args) => args,
176+
Err(error) => return Some(Err(error)),
177+
};
178+
let mut cmd = QueueCommand {
179+
include: args.remove("include"),
180+
exclude: args.remove("exclude"),
181+
runs: None,
182+
};
183+
if let Some(runs) = args.remove("runs") {
184+
let Ok(runs) = runs.parse::<u32>() else {
185+
return Some(Err(format!("Cannot parse runs {runs} as a number")));
186+
};
187+
cmd.runs = Some(runs as i32);
188+
}
189+
190+
if let Some((key, _)) = args.into_iter().next() {
191+
return Some(Err(format!("Unknown command argument {key}")));
192+
}
193+
194+
Some(Ok(cmd))
195+
}
196+
197+
/// Parses command arguments from a single line of text.
198+
/// Expects that arguments are separated by whitespace, and each argument
199+
/// has the format `<key>=<value>`.
200+
fn parse_command_arguments(args: &str) -> Result<HashMap<&str, &str>, String> {
201+
let mut argmap = HashMap::new();
202+
for arg in args.split_whitespace() {
203+
let Some((key, value)) = arg.split_once('=') else {
204+
return Err(format!(
205+
"Invalid command argument `{arg}` (there may be no spaces around the `=` character)"
206+
));
207+
};
208+
let key = key.trim();
209+
let value = value.trim();
210+
if argmap.insert(key, value).is_some() {
211+
return Err(format!("Duplicate command argument `{key}`"));
212+
}
213+
}
214+
215+
Ok(argmap)
216+
}
217+
218+
#[derive(Debug)]
219+
struct QueueCommand<'a> {
220+
include: Option<&'a str>,
221+
exclude: Option<&'a str>,
222+
runs: Option<i32>,
223+
}
224+
166225
/// Run the `@rust-timer build` regex over the comment message extracting the commit and the other captures
167226
fn build_captures(comment_body: &str) -> impl Iterator<Item = (&str, regex::Captures)> {
168227
BODY_TIMER_BUILD
@@ -196,6 +255,7 @@ pub async fn get_authorized_users() -> Result<Vec<u64>, String> {
196255
#[cfg(test)]
197256
mod tests {
198257
use super::*;
258+
199259
#[test]
200260
fn captures_all_shas() {
201261
let comment_body = r#"
@@ -215,4 +275,80 @@ Going to do perf runs for a few of these:
215275
]
216276
);
217277
}
278+
279+
#[test]
280+
fn command_missing() {
281+
assert!(parse_queue_command("").is_none());
282+
}
283+
284+
#[test]
285+
fn unknown_command() {
286+
assert!(parse_queue_command("@rust-timer foo").is_none());
287+
}
288+
289+
#[test]
290+
fn queue_command() {
291+
insta::assert_compact_debug_snapshot!(parse_queue_command("@rust-timer queue"),
292+
@"Some(Ok(QueueCommand { include: None, exclude: None, runs: None }))");
293+
}
294+
295+
#[test]
296+
fn queue_command_unknown_arg() {
297+
insta::assert_compact_debug_snapshot!(parse_queue_command("@rust-timer queue foo=bar"),
298+
@r###"Some(Err("Unknown command argument foo"))"###);
299+
}
300+
301+
#[test]
302+
fn queue_command_duplicate_arg() {
303+
insta::assert_compact_debug_snapshot!(parse_queue_command("@rust-timer queue include=a exclude=c include=b"),
304+
@r###"Some(Err("Duplicate command argument `include`"))"###);
305+
}
306+
307+
#[test]
308+
fn queue_command_include() {
309+
insta::assert_compact_debug_snapshot!(parse_queue_command("@rust-timer queue include=abcd,feih"),
310+
@r###"Some(Ok(QueueCommand { include: Some("abcd,feih"), exclude: None, runs: None }))"###);
311+
}
312+
313+
#[test]
314+
fn queue_command_exclude() {
315+
insta::assert_compact_debug_snapshot!(parse_queue_command("@rust-timer queue exclude=foo134,barzbaz41baf"),
316+
@r###"Some(Ok(QueueCommand { include: None, exclude: Some("foo134,barzbaz41baf"), runs: None }))"###);
317+
}
318+
319+
#[test]
320+
fn queue_command_runs() {
321+
insta::assert_compact_debug_snapshot!(parse_queue_command("@rust-timer queue runs=5"),
322+
@"Some(Ok(QueueCommand { include: None, exclude: None, runs: Some(5) }))");
323+
}
324+
325+
#[test]
326+
fn queue_command_runs_nan() {
327+
insta::assert_compact_debug_snapshot!(parse_queue_command("@rust-timer queue runs=xxx"),
328+
@r###"Some(Err("Cannot parse runs xxx as a number"))"###);
329+
}
330+
331+
#[test]
332+
fn queue_command_combination() {
333+
insta::assert_compact_debug_snapshot!(parse_queue_command("@rust-timer queue include=acda,13asd exclude=c13,DA runs=5"),
334+
@r###"Some(Ok(QueueCommand { include: Some("acda,13asd"), exclude: Some("c13,DA"), runs: Some(5) }))"###);
335+
}
336+
337+
#[test]
338+
fn queue_command_argument_spaces() {
339+
insta::assert_compact_debug_snapshot!(parse_queue_command("@rust-timer queue include = abcd,das"),
340+
@r###"Some(Err("Invalid command argument `include` (there may be no spaces around the `=` character)"))"###);
341+
}
342+
343+
#[test]
344+
fn queue_command_spaces() {
345+
insta::assert_compact_debug_snapshot!(parse_queue_command("@rust-timer queue include=abcd,das "),
346+
@r###"Some(Ok(QueueCommand { include: Some("abcd,das"), exclude: None, runs: None }))"###);
347+
}
348+
349+
#[test]
350+
fn queue_command_with_bors() {
351+
insta::assert_compact_debug_snapshot!(parse_queue_command("@bors try @rust-timer queue include=foo,bar"),
352+
@r###"Some(Ok(QueueCommand { include: Some("foo,bar"), exclude: None, runs: None }))"###);
353+
}
218354
}

0 commit comments

Comments
 (0)