1
1
use std:: borrow:: Cow ;
2
+ use std:: process:: ExitCode ;
2
3
use std:: sync:: OnceLock ;
3
- use std:: { env, fmt , fs} ;
4
+ use std:: { env, fs} ;
4
5
5
- use jsonpath_lib:: select;
6
6
use regex:: { Regex , RegexBuilder } ;
7
7
use serde_json:: Value ;
8
8
@@ -14,90 +14,134 @@ use cache::Cache;
14
14
use config:: parse_config;
15
15
use error:: CkError ;
16
16
17
- fn main ( ) -> Result < ( ) , String > {
17
+ fn main ( ) -> ExitCode {
18
18
let config = parse_config ( env:: args ( ) . collect ( ) ) ;
19
19
20
20
let mut failed = Vec :: new ( ) ;
21
21
let mut cache = Cache :: new ( & config) ;
22
- let commands = get_commands ( & config. template )
23
- . map_err ( |_| format ! ( "Jsondocck failed for {}" , & config. template) ) ?;
22
+ let Ok ( commands) = get_commands ( & config. template ) else {
23
+ eprintln ! ( "Jsondocck failed for {}" , & config. template) ;
24
+ return ExitCode :: FAILURE ;
25
+ } ;
24
26
25
27
for command in commands {
26
- if let Err ( e ) = check_command ( command, & mut cache) {
27
- failed. push ( e ) ;
28
+ if let Err ( message ) = check_command ( & command, & mut cache) {
29
+ failed. push ( CkError { command , message } ) ;
28
30
}
29
31
}
30
32
31
33
if failed. is_empty ( ) {
32
- Ok ( ( ) )
34
+ ExitCode :: SUCCESS
33
35
} else {
34
36
for i in failed {
35
- eprintln ! ( "{}" , i) ;
37
+ eprintln ! ( "{}:{}, command failed" , config. template, i. command. lineno) ;
38
+ eprintln ! ( "{}" , i. message)
36
39
}
37
- Err ( format ! ( "Jsondocck failed for {}" , & config . template ) )
40
+ ExitCode :: FAILURE
38
41
}
39
42
}
40
43
41
44
#[ derive( Debug ) ]
42
45
pub struct Command {
43
- negated : bool ,
44
46
kind : CommandKind ,
45
- args : Vec < String > ,
47
+ path : String ,
46
48
lineno : usize ,
47
49
}
48
50
49
51
#[ derive( Debug ) ]
50
- pub enum CommandKind {
51
- Has ,
52
- Count ,
53
- Is ,
54
- IsMany ,
55
- Set ,
52
+ enum CommandKind {
53
+ /// `//@ has <path>`
54
+ ///
55
+ /// Checks the path exists.
56
+ HasPath ,
57
+
58
+ /// `//@ has <path> <value>`
59
+ ///
60
+ /// Check one thing at the path is equal to the value.
61
+ HasValue { value : String } ,
62
+
63
+ /// `//@ !has <path>`
64
+ ///
65
+ /// Checks the path doesn't exist.
66
+ HasNotPath ,
67
+
68
+ /// `//@ is <path> <value>`
69
+ ///
70
+ /// Check the path is the given value.
71
+ Is { value : String } ,
72
+
73
+ /// `//@ is <path> <value> <value>...`
74
+ ///
75
+ /// Check that the path matches to exactly every given value.
76
+ IsMany { values : Vec < String > } ,
77
+
78
+ /// `//@ !is <path> <value>`
79
+ ///
80
+ /// Check the path isn't the given value.
81
+ IsNot { value : String } ,
82
+
83
+ /// `//@ count <path> <value>`
84
+ ///
85
+ /// Check the path has the expected number of matches.
86
+ CountIs { expected : usize } ,
87
+
88
+ /// `//@ set <name> = <path>`
89
+ Set { variable : String } ,
56
90
}
57
91
58
92
impl CommandKind {
59
- fn validate ( & self , args : & [ String ] , lineno : usize ) -> bool {
60
- // FIXME(adotinthevoid): We should "parse, don't validate" here, so we avoid ad-hoc
61
- // indexing in check_command.
62
- let count = match self {
63
- CommandKind :: Has => ( 1 ..=2 ) . contains ( & args. len ( ) ) ,
64
- CommandKind :: IsMany => args. len ( ) >= 2 ,
65
- CommandKind :: Count | CommandKind :: Is => 2 == args. len ( ) ,
66
- CommandKind :: Set => 3 == args. len ( ) ,
67
- } ;
93
+ /// Returns both the kind and the path.
94
+ ///
95
+ /// Returns `None` if the command isn't from jsondocck (e.g. from compiletest).
96
+ fn parse < ' a > ( command_name : & str , negated : bool , args : & ' a [ String ] ) -> Option < ( Self , & ' a str ) > {
97
+ let kind = match ( command_name, negated) {
98
+ ( "count" , false ) => {
99
+ assert_eq ! ( args. len( ) , 2 ) ;
100
+ let expected = args[ 1 ] . parse ( ) . expect ( "invalid number for `count`" ) ;
101
+ Self :: CountIs { expected }
102
+ }
68
103
69
- if !count {
70
- print_err ( & format ! ( "Incorrect number of arguments to `{}`" , self ) , lineno) ;
71
- return false ;
72
- }
104
+ ( "ismany" , false ) => {
105
+ // FIXME: Make this >= 3, and migrate len(values)==1 cases to @is
106
+ assert ! ( args. len( ) >= 2 , "Not enough args to `ismany`" ) ;
107
+ let values = args[ 1 ..] . to_owned ( ) ;
108
+ Self :: IsMany { values }
109
+ }
73
110
74
- if let CommandKind :: Count = self {
75
- if args[ 1 ] . parse :: < usize > ( ) . is_err ( ) {
76
- print_err (
77
- & format ! (
78
- "Second argument to `count` must be a valid usize (got `{}`)" ,
79
- args[ 1 ]
80
- ) ,
81
- lineno,
82
- ) ;
83
- return false ;
111
+ ( "is" , false ) => {
112
+ assert_eq ! ( args. len( ) , 2 ) ;
113
+ Self :: Is { value : args[ 1 ] . clone ( ) }
114
+ }
115
+ ( "is" , true ) => {
116
+ assert_eq ! ( args. len( ) , 2 ) ;
117
+ Self :: IsNot { value : args[ 1 ] . clone ( ) }
84
118
}
85
- }
86
119
87
- true
88
- }
89
- }
120
+ ( "set" , false ) => {
121
+ assert_eq ! ( args. len( ) , 3 ) ;
122
+ assert_eq ! ( args[ 1 ] , "=" ) ;
123
+ return Some ( ( Self :: Set { variable : args[ 0 ] . clone ( ) } , & args[ 2 ] ) ) ;
124
+ }
90
125
91
- impl fmt:: Display for CommandKind {
92
- fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
93
- let text = match self {
94
- CommandKind :: Has => "has" ,
95
- CommandKind :: IsMany => "ismany" ,
96
- CommandKind :: Count => "count" ,
97
- CommandKind :: Is => "is" ,
98
- CommandKind :: Set => "set" ,
126
+ ( "has" , false ) => match args {
127
+ [ _path] => Self :: HasPath ,
128
+ [ _path, value] => Self :: HasValue { value : value. clone ( ) } ,
129
+ _ => panic ! ( "`//@ has` must have 2 or 3 arguments, but got {args:?}" ) ,
130
+ } ,
131
+ ( "has" , true ) => {
132
+ assert_eq ! ( args. len( ) , 1 , "args={args:?}" ) ;
133
+ Self :: HasNotPath
134
+ }
135
+
136
+ ( _, false ) if KNOWN_DIRECTIVE_NAMES . contains ( & command_name) => {
137
+ return None ;
138
+ }
139
+ _ => {
140
+ panic ! ( "Invalid command `//@ {}{command_name}`" , if negated { "!" } else { "" } )
141
+ }
99
142
} ;
100
- write ! ( f, "{}" , text)
143
+
144
+ Some ( ( kind, & args[ 0 ] ) )
101
145
}
102
146
}
103
147
@@ -125,8 +169,7 @@ fn print_err(msg: &str, lineno: usize) {
125
169
// See <https://github.com/rust-lang/rust/issues/125813#issuecomment-2141953780>.
126
170
include ! ( concat!( env!( "CARGO_MANIFEST_DIR" ) , "/../compiletest/src/directive-list.rs" ) ) ;
127
171
128
- /// Get a list of commands from a file. Does the work of ensuring the commands
129
- /// are syntactically valid.
172
+ /// Get a list of commands from a file.
130
173
fn get_commands ( template : & str ) -> Result < Vec < Command > , ( ) > {
131
174
let mut commands = Vec :: new ( ) ;
132
175
let mut errors = false ;
@@ -142,217 +185,102 @@ fn get_commands(template: &str) -> Result<Vec<Command>, ()> {
142
185
143
186
let negated = cap. name ( "negated" ) . unwrap ( ) . as_str ( ) == "!" ;
144
187
145
- let cmd = match cap. name ( "cmd" ) . unwrap ( ) . as_str ( ) {
146
- "has" => CommandKind :: Has ,
147
- "count" => CommandKind :: Count ,
148
- "is" => CommandKind :: Is ,
149
- "ismany" => CommandKind :: IsMany ,
150
- "set" => CommandKind :: Set ,
151
- // FIXME: See the comment above the `include!(...)`.
152
- cmd if KNOWN_DIRECTIVE_NAMES . contains ( & cmd) => continue ,
153
- cmd => {
154
- print_err ( & format ! ( "Unrecognized command name `{cmd}`" ) , lineno) ;
155
- errors = true ;
156
- continue ;
157
- }
158
- } ;
159
-
160
- let args = cap. name ( "args" ) . map_or ( Some ( vec ! [ ] ) , |m| shlex:: split ( m. as_str ( ) ) ) ;
161
-
162
- let args = match args {
188
+ let args_str = & cap[ "args" ] ;
189
+ let args = match shlex:: split ( args_str) {
163
190
Some ( args) => args,
164
191
None => {
165
- print_err (
166
- & format ! (
167
- "Invalid arguments to shlex::split: `{}`" ,
168
- cap. name( "args" ) . unwrap( ) . as_str( )
169
- ) ,
170
- lineno,
171
- ) ;
192
+ print_err ( & format ! ( "Invalid arguments to shlex::split: `{args_str}`" , ) , lineno) ;
172
193
errors = true ;
173
194
continue ;
174
195
}
175
196
} ;
176
197
177
- if !cmd. validate ( & args, lineno) {
178
- errors = true ;
179
- continue ;
198
+ if let Some ( ( kind, path) ) = CommandKind :: parse ( & cap[ "cmd" ] , negated, & args) {
199
+ commands. push ( Command { kind, lineno, path : path. to_owned ( ) } )
180
200
}
181
-
182
- commands. push ( Command { negated, kind : cmd, args, lineno } )
183
201
}
184
202
185
203
if !errors { Ok ( commands) } else { Err ( ( ) ) }
186
204
}
187
205
188
- /// Performs the actual work of ensuring a command passes. Generally assumes the command
189
- /// is syntactically valid.
190
- fn check_command ( command : Command , cache : & mut Cache ) -> Result < ( ) , CkError > {
191
- // FIXME: Be more granular about why, (e.g. syntax error, count not equal)
192
- let result = match command. kind {
193
- CommandKind :: Has => {
194
- match command. args . len ( ) {
195
- // `has <jsonpath>`: Check that `jsonpath` exists.
196
- 1 => {
197
- let val = cache. value ( ) ;
198
- let results = select ( val, & command. args [ 0 ] ) . unwrap ( ) ;
199
- !results. is_empty ( )
200
- }
201
- // `has <jsonpath> <value>`: Check *any* item matched by `jsonpath` equals `value`.
202
- 2 => {
203
- let val = cache. value ( ) . clone ( ) ;
204
- let results = select ( & val, & command. args [ 0 ] ) . unwrap ( ) ;
205
- let pat = string_to_value ( & command. args [ 1 ] , cache) ;
206
- let has = results. contains ( & pat. as_ref ( ) ) ;
207
- // Give better error for when `has` check fails.
208
- if !command. negated && !has {
209
- return Err ( CkError :: FailedCheck (
210
- format ! (
211
- "{} matched to {:?} but didn't have {:?}" ,
212
- & command. args[ 0 ] ,
213
- results,
214
- pat. as_ref( )
215
- ) ,
216
- command,
217
- ) ) ;
218
- } else {
219
- has
220
- }
221
- }
222
- _ => unreachable ! ( ) ,
206
+ /// Performs the actual work of ensuring a command passes.
207
+ fn check_command ( command : & Command , cache : & mut Cache ) -> Result < ( ) , String > {
208
+ let matches = cache. select ( & command. path ) ;
209
+ match & command. kind {
210
+ CommandKind :: HasPath => {
211
+ if matches. is_empty ( ) {
212
+ return Err ( "matched to no values" . to_owned ( ) ) ;
213
+ }
214
+ }
215
+ CommandKind :: HasNotPath => {
216
+ if !matches. is_empty ( ) {
217
+ return Err ( format ! ( "matched to {matches:?}, but wanted no matches" ) ) ;
218
+ }
219
+ }
220
+ CommandKind :: HasValue { value } => {
221
+ let want_value = string_to_value ( value, cache) ;
222
+ if !matches. contains ( & want_value. as_ref ( ) ) {
223
+ return Err ( format ! ( "matched to {matches:?}, which didn't contain {want_value:?}" ) ) ;
224
+ }
225
+ }
226
+ CommandKind :: Is { value } => {
227
+ let want_value = string_to_value ( value, cache) ;
228
+ let matched = get_one ( & matches) ?;
229
+ if matched != want_value. as_ref ( ) {
230
+ return Err ( format ! ( "matched to {matched:?} but want {want_value:?}" ) ) ;
231
+ }
232
+ }
233
+ CommandKind :: IsNot { value } => {
234
+ let wantnt_value = string_to_value ( value, cache) ;
235
+ let matched = get_one ( & matches) ?;
236
+ if matched == wantnt_value. as_ref ( ) {
237
+ return Err ( format ! ( "got value {wantnt_value:?}, but want anything else" ) ) ;
223
238
}
224
239
}
225
- // `ismany <path> <jsonpath> <value...>`
226
- CommandKind :: IsMany => {
227
- assert ! ( !command. negated, "`ismany` may not be negated" ) ;
228
- let ( query, values) = if let [ query, values @ ..] = & command. args [ ..] {
229
- ( query, values)
230
- } else {
231
- unreachable ! ( "Checked in CommandKind::validate" )
232
- } ;
233
- let val = cache. value ( ) ;
234
- let got_values = select ( val, & query) . unwrap ( ) ;
235
240
241
+ CommandKind :: IsMany { values } => {
236
242
// Serde json doesn't implement Ord or Hash for Value, so we must
237
243
// use a Vec here. While in theory that makes setwize equality
238
244
// O(n^2), in practice n will never be large enough to matter.
239
245
let expected_values =
240
246
values. iter ( ) . map ( |v| string_to_value ( v, cache) ) . collect :: < Vec < _ > > ( ) ;
241
- if expected_values. len ( ) != got_values. len ( ) {
242
- return Err ( CkError :: FailedCheck (
243
- format ! (
244
- "Expected {} values, but `{}` matched to {} values ({:?})" ,
245
- expected_values. len( ) ,
246
- query,
247
- got_values. len( ) ,
248
- got_values
249
- ) ,
250
- command,
247
+ if expected_values. len ( ) != matches. len ( ) {
248
+ return Err ( format ! (
249
+ "Expected {} values, but matched to {} values ({:?})" ,
250
+ expected_values. len( ) ,
251
+ matches. len( ) ,
252
+ matches
251
253
) ) ;
252
254
} ;
253
- for got_value in got_values {
255
+ for got_value in matches {
254
256
if !expected_values. iter ( ) . any ( |exp| & * * exp == got_value) {
255
- return Err ( CkError :: FailedCheck (
256
- format ! ( "`{}` has match {:?}, which was not expected" , query, got_value) ,
257
- command,
258
- ) ) ;
257
+ return Err ( format ! ( "has match {got_value:?}, which was not expected" , ) ) ;
259
258
}
260
259
}
261
- true
262
- }
263
- // `count <jsonpath> <count>`: Check that `jsonpath` matches exactly `count` times.
264
- CommandKind :: Count => {
265
- assert_eq ! ( command. args. len( ) , 2 ) ;
266
- let expected: usize = command. args [ 1 ] . parse ( ) . unwrap ( ) ;
267
- let val = cache. value ( ) ;
268
- let results = select ( val, & command. args [ 0 ] ) . unwrap ( ) ;
269
- let eq = results. len ( ) == expected;
270
- if !command. negated && !eq {
271
- return Err ( CkError :: FailedCheck (
272
- format ! (
273
- "`{}` matched to `{:?}` with length {}, but expected length {}" ,
274
- & command. args[ 0 ] ,
275
- results,
276
- results. len( ) ,
277
- expected
278
- ) ,
279
- command,
280
- ) ) ;
281
- } else {
282
- eq
283
- }
284
260
}
285
- // `has <jsonpath> <value>`: Check` *exactly one* item matched by `jsonpath`, and it equals `value`.
286
- CommandKind :: Is => {
287
- assert_eq ! ( command. args. len( ) , 2 ) ;
288
- let val = cache. value ( ) . clone ( ) ;
289
- let results = select ( & val, & command. args [ 0 ] ) . unwrap ( ) ;
290
- let pat = string_to_value ( & command. args [ 1 ] , cache) ;
291
- let is = results. len ( ) == 1 && results[ 0 ] == pat. as_ref ( ) ;
292
- if !command. negated && !is {
293
- return Err ( CkError :: FailedCheck (
294
- format ! (
295
- "{} matched to {:?}, but expected {:?}" ,
296
- & command. args[ 0 ] ,
297
- results,
298
- pat. as_ref( )
299
- ) ,
300
- command,
261
+ CommandKind :: CountIs { expected } => {
262
+ if * expected != matches. len ( ) {
263
+ return Err ( format ! (
264
+ "matched to `{matches:?}` with length {}, but expected length {expected}" ,
265
+ matches. len( ) ,
301
266
) ) ;
302
- } else {
303
- is
304
267
}
305
268
}
306
- // `set <name> = <jsonpath>`
307
- CommandKind :: Set => {
308
- assert ! ( !command. negated, "`set` may not be negated" ) ;
309
- assert_eq ! ( command. args. len( ) , 3 ) ;
310
- assert_eq ! ( command. args[ 1 ] , "=" , "Expected an `=`" ) ;
311
- let val = cache. value ( ) . clone ( ) ;
312
- let results = select ( & val, & command. args [ 2 ] ) . unwrap ( ) ;
313
- assert_eq ! (
314
- results. len( ) ,
315
- 1 ,
316
- "Expected 1 match for `{}` (because of `set`): matched to {:?}" ,
317
- command. args[ 2 ] ,
318
- results
319
- ) ;
320
- match results. len ( ) {
321
- 0 => false ,
322
- 1 => {
323
- let r = cache. variables . insert ( command. args [ 0 ] . clone ( ) , results[ 0 ] . clone ( ) ) ;
324
- assert ! ( r. is_none( ) , "Name collision: {} is duplicated" , command. args[ 0 ] ) ;
325
- true
326
- }
327
- _ => {
328
- panic ! (
329
- "Got multiple results in `set` for `{}`: {:?}" ,
330
- & command. args[ 2 ] , results,
331
- ) ;
332
- }
333
- }
269
+ CommandKind :: Set { variable } => {
270
+ let value = get_one ( & matches) ?;
271
+ let r = cache. variables . insert ( variable. to_owned ( ) , value. clone ( ) ) ;
272
+ assert ! ( r. is_none( ) , "name collision: {variable:?} is duplicated" ) ;
334
273
}
335
- } ;
274
+ }
336
275
337
- if result == command. negated {
338
- if command. negated {
339
- Err ( CkError :: FailedCheck (
340
- format ! ( "`!{} {}` matched when it shouldn't" , command. kind, command. args. join( " " ) ) ,
341
- command,
342
- ) )
343
- } else {
344
- // FIXME: In the future, try 'peeling back' each step, and see at what level the match failed
345
- Err ( CkError :: FailedCheck (
346
- format ! (
347
- "`{} {}` didn't match when it should" ,
348
- command. kind,
349
- command. args. join( " " )
350
- ) ,
351
- command,
352
- ) )
353
- }
354
- } else {
355
- Ok ( ( ) )
276
+ Ok ( ( ) )
277
+ }
278
+
279
+ fn get_one < ' a > ( matches : & [ & ' a Value ] ) -> Result < & ' a Value , String > {
280
+ match matches {
281
+ [ ] => Err ( "matched to no values" . to_owned ( ) ) ,
282
+ [ matched] => Ok ( matched) ,
283
+ _ => Err ( format ! ( "matched to multiple values {matches:?}, but want exactly 1" ) ) ,
356
284
}
357
285
}
358
286
0 commit comments