@@ -5,15 +5,13 @@ use crate::github::{
5
5
} ;
6
6
use crate :: load:: SiteCtxt ;
7
7
8
- use std:: sync:: Arc ;
9
-
8
+ use hashbrown:: HashMap ;
10
9
use regex:: Regex ;
10
+ use std:: sync:: Arc ;
11
11
12
12
lazy_static:: lazy_static! {
13
13
static ref BODY_TIMER_BUILD : Regex =
14
14
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( ) ;
17
15
}
18
16
19
17
pub async fn handle_github (
@@ -118,26 +116,25 @@ async fn handle_rust_timer(
118
116
return Ok ( github:: Response ) ;
119
117
}
120
118
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 ;
132
125
format ! (
133
126
"Awaiting bors try build completion.
134
127
135
128
@rustbot label: +S-waiting-on-perf
136
129
137
130
{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 ;
141
138
return Ok ( github:: Response ) ;
142
139
}
143
140
@@ -163,6 +160,64 @@ async fn handle_rust_timer(
163
160
Ok ( github:: Response )
164
161
}
165
162
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
+
166
221
/// Run the `@rust-timer build` regex over the comment message extracting the commit and the other captures
167
222
fn build_captures ( comment_body : & str ) -> impl Iterator < Item = ( & str , regex:: Captures ) > {
168
223
BODY_TIMER_BUILD
@@ -196,6 +251,7 @@ pub async fn get_authorized_users() -> Result<Vec<u64>, String> {
196
251
#[ cfg( test) ]
197
252
mod tests {
198
253
use super :: * ;
254
+
199
255
#[ test]
200
256
fn captures_all_shas ( ) {
201
257
let comment_body = r#"
@@ -215,4 +271,68 @@ Going to do perf runs for a few of these:
215
271
]
216
272
) ;
217
273
}
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
+ }
218
338
}
0 commit comments