@@ -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,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
167222fn 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) ]
197252mod 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