Skip to content

Commit d852d3a

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 d852d3a

File tree

3 files changed

+160
-18
lines changed

3 files changed

+160
-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: 138 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,64 @@ 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 bot_line = body
167+
.lines()
168+
.find_map(|line| line.trim().strip_prefix("@rust-timer"))
169+
.map(|l| l.trim())?;
170+
171+
let args = bot_line.strip_prefix("queue").map(|l| l.trim())?;
172+
let mut args = match parse_command_arguments(args) {
173+
Ok(args) => args,
174+
Err(error) => return Some(Err(error)),
175+
};
176+
let mut cmd = QueueCommand::default();
177+
cmd.include = args.remove("include");
178+
cmd.exclude = args.remove("exclude");
179+
if let Some(runs) = args.remove("runs") {
180+
let Ok(runs) = runs.parse::<u32>() else {
181+
return Some(Err(format!("Cannot parse runs {runs} as a number")));
182+
};
183+
cmd.runs = Some(runs as i32);
184+
}
185+
186+
if let Some((key, _)) = args.into_iter().next() {
187+
return Some(Err(format!("Unknown command argument {key}")));
188+
}
189+
190+
Some(Ok(cmd))
191+
}
192+
193+
/// Parses command arguments from a single line of text.
194+
/// Expects that arguments are separated by whitespace, and each argument
195+
/// has the format `<key>=<value>`.
196+
fn parse_command_arguments(args: &str) -> Result<HashMap<&str, &str>, String> {
197+
let mut argmap = HashMap::new();
198+
for arg in args.split_whitespace() {
199+
let Some((key, value)) = arg.split_once("=") else {
200+
return Err(format!(
201+
"Invalid command argument `{arg}` (there may be no spaces around the `=` character)"
202+
));
203+
};
204+
let key = key.trim();
205+
let value = value.trim();
206+
if argmap.insert(key, value).is_some() {
207+
return Err(format!("Duplicate command argument `{key}`"));
208+
}
209+
}
210+
211+
Ok(argmap)
212+
}
213+
214+
#[derive(Default, Debug)]
215+
struct QueueCommand<'a> {
216+
include: Option<&'a str>,
217+
exclude: Option<&'a str>,
218+
runs: Option<i32>,
219+
}
220+
166221
/// Run the `@rust-timer build` regex over the comment message extracting the commit and the other captures
167222
fn build_captures(comment_body: &str) -> impl Iterator<Item = (&str, regex::Captures)> {
168223
BODY_TIMER_BUILD
@@ -196,6 +251,7 @@ pub async fn get_authorized_users() -> Result<Vec<u64>, String> {
196251
#[cfg(test)]
197252
mod tests {
198253
use super::*;
254+
199255
#[test]
200256
fn captures_all_shas() {
201257
let comment_body = r#"
@@ -215,4 +271,68 @@ Going to do perf runs for a few of these:
215271
]
216272
);
217273
}
274+
275+
#[test]
276+
fn command_missing() {
277+
assert!(parse_queue_command("").is_none());
278+
}
279+
280+
#[test]
281+
fn unknown_command() {
282+
assert!(parse_queue_command("@rust-timer foo").is_none());
283+
}
284+
285+
#[test]
286+
fn queue_command() {
287+
insta::assert_compact_debug_snapshot!(parse_queue_command("@rust-timer queue"),
288+
@"Some(Ok(QueueCommand { include: None, exclude: None, runs: None }))");
289+
}
290+
291+
#[test]
292+
fn queue_command_unknown_arg() {
293+
insta::assert_compact_debug_snapshot!(parse_queue_command("@rust-timer queue foo=bar"),
294+
@r###"Some(Err("Unknown command argument foo"))"###);
295+
}
296+
297+
#[test]
298+
fn queue_command_duplicate_arg() {
299+
insta::assert_compact_debug_snapshot!(parse_queue_command("@rust-timer queue include=a exclude=c include=b"),
300+
@r###"Some(Err("Duplicate command argument `include`"))"###);
301+
}
302+
303+
#[test]
304+
fn queue_command_include() {
305+
insta::assert_compact_debug_snapshot!(parse_queue_command("@rust-timer queue include=abcd,feih"),
306+
@r###"Some(Ok(QueueCommand { include: Some("abcd,feih"), exclude: None, runs: None }))"###);
307+
}
308+
309+
#[test]
310+
fn queue_command_exclude() {
311+
insta::assert_compact_debug_snapshot!(parse_queue_command("@rust-timer queue exclude=foo134,barzbaz41baf"),
312+
@r###"Some(Ok(QueueCommand { include: None, exclude: Some("foo134,barzbaz41baf"), runs: None }))"###);
313+
}
314+
315+
#[test]
316+
fn queue_command_runs() {
317+
insta::assert_compact_debug_snapshot!(parse_queue_command("@rust-timer queue runs=5"),
318+
@"Some(Ok(QueueCommand { include: None, exclude: None, runs: Some(5) }))");
319+
}
320+
321+
#[test]
322+
fn queue_command_runs_nan() {
323+
insta::assert_compact_debug_snapshot!(parse_queue_command("@rust-timer queue runs=xxx"),
324+
@r###"Some(Err("Cannot parse runs xxx as a number"))"###);
325+
}
326+
327+
#[test]
328+
fn queue_command_combination() {
329+
insta::assert_compact_debug_snapshot!(parse_queue_command("@rust-timer queue include=acda,13asd exclude=c13,DA runs=5"),
330+
@r###"Some(Ok(QueueCommand { include: Some("acda,13asd"), exclude: Some("c13,DA"), runs: Some(5) }))"###);
331+
}
332+
333+
#[test]
334+
fn queue_command_spaces() {
335+
insta::assert_compact_debug_snapshot!(parse_queue_command("@rust-timer queue include = abcd,das exclude=ada21"),
336+
@r###"Some(Err("Invalid command argument `include` (there may be no spaces around the `=` character)"))"###);
337+
}
218338
}

0 commit comments

Comments
 (0)