@@ -5,15 +5,13 @@ use crate::github::{
55} ;
66use crate :: load:: SiteCtxt ;
77
8- use std:: sync:: Arc ;
9-
8+ use hashbrown:: HashMap ;
109use regex:: Regex ;
10+ use std:: sync:: Arc ;
1111
1212lazy_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
1917pub 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
167226fn 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) ]
197256mod 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