• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

loot / loot-condition-interpreter / 14716450737

28 Apr 2025 05:18PM UTC coverage: 91.492% (-0.1%) from 91.6%
14716450737

push

github

Ortham
Update versions and changelog for v5.3.2

4850 of 5301 relevant lines covered (91.49%)

15.15 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.65
/src/function/parse.rs
1
use std::path::PathBuf;
2
use std::str;
3

4
use nom::branch::alt;
5
use nom::bytes::complete::{is_not, tag};
6
use nom::character::complete::digit1;
7
use nom::character::complete::hex_digit1;
8
use nom::combinator::{map, map_parser, value};
9
use nom::sequence::delimited;
10
use nom::{Err, IResult, Parser};
11
use regex::{Regex, RegexBuilder};
12

13
use super::{ComparisonOperator, Function};
14
use crate::error::ParsingErrorKind;
15
use crate::{map_err, whitespace, ParsingResult};
16

17
impl ComparisonOperator {
18
    pub fn parse(input: &str) -> IResult<&str, ComparisonOperator> {
16✔
19
        alt((
16✔
20
            value(ComparisonOperator::Equal, tag("==")),
16✔
21
            value(ComparisonOperator::NotEqual, tag("!=")),
16✔
22
            value(ComparisonOperator::LessThanOrEqual, tag("<=")),
16✔
23
            value(ComparisonOperator::GreaterThanOrEqual, tag(">=")),
16✔
24
            value(ComparisonOperator::LessThan, tag("<")),
16✔
25
            value(ComparisonOperator::GreaterThan, tag(">")),
16✔
26
        ))
16✔
27
        .parse(input)
16✔
28
    }
16✔
29
}
30

31
const INVALID_PATH_CHARS: &str = "\":*?<>|";
32
const INVALID_NON_REGEX_PATH_CHARS: &str = "\":*?<>|\\"; // \ is treated as invalid to distinguish regex strings.
33
const INVALID_REGEX_PATH_CHARS: &str = "\"<>";
34

35
fn build_regex(input: &str) -> Result<(&'static str, Regex), regex::Error> {
16✔
36
    RegexBuilder::new(input)
16✔
37
        .case_insensitive(true)
16✔
38
        .build()
16✔
39
        .map(|r| ("", r))
16✔
40
}
16✔
41

42
fn parse_regex(input: &str) -> ParsingResult<Regex> {
3✔
43
    build_regex(input).map_err(|e| Err::Failure(ParsingErrorKind::from(e).at(input)))
3✔
44
}
3✔
45

46
fn parse_anchored_regex(input: &str) -> ParsingResult<Regex> {
13✔
47
    build_regex(&format!("^{input}$"))
13✔
48
        .map_err(|e| Err::Failure(ParsingErrorKind::from(e).at(input)))
13✔
49
}
13✔
50

51
fn parse_path(input: &str) -> IResult<&str, PathBuf> {
28✔
52
    map(
28✔
53
        delimited(tag("\""), is_not(INVALID_PATH_CHARS), tag("\"")),
28✔
54
        PathBuf::from,
28✔
55
    )
28✔
56
    .parse(input)
28✔
57
}
28✔
58

59
fn parse_size(input: &str) -> ParsingResult<u64> {
1✔
60
    str::parse(input)
1✔
61
        .map(|c| ("", c))
1✔
62
        .map_err(|e| Err::Failure(ParsingErrorKind::from(e).at(input)))
1✔
63
}
1✔
64

65
fn parse_file_size_args(input: &str) -> ParsingResult<(PathBuf, u64)> {
1✔
66
    let mut parser = (
1✔
67
        map_err(parse_path),
1✔
68
        map_err(whitespace(tag(","))),
1✔
69
        map_parser(digit1, parse_size),
1✔
70
    );
1✔
71

72
    let (remaining_input, (path, _, size)) = parser.parse(input)?;
1✔
73

74
    Ok((remaining_input, (path, size)))
1✔
75
}
1✔
76

77
fn parse_version(input: &str) -> IResult<&str, String> {
16✔
78
    map(
16✔
79
        delimited(tag("\""), is_not("\""), tag("\"")),
16✔
80
        |version: &str| version.to_owned(),
16✔
81
    )
16✔
82
    .parse(input)
16✔
83
}
16✔
84

85
fn parse_version_args(input: &str) -> ParsingResult<(PathBuf, String, ComparisonOperator)> {
14✔
86
    let parser = (
14✔
87
        parse_path,
14✔
88
        whitespace(tag(",")),
14✔
89
        parse_version,
14✔
90
        whitespace(tag(",")),
14✔
91
        ComparisonOperator::parse,
14✔
92
    );
14✔
93

94
    let (remaining_input, (path, _, version, _, comparator)) = map_err(parser).parse(input)?;
14✔
95

96
    Ok((remaining_input, (path, version, comparator)))
14✔
97
}
14✔
98

99
fn parse_filename_version_args(
2✔
100
    input: &str,
2✔
101
) -> ParsingResult<(PathBuf, Regex, String, ComparisonOperator)> {
2✔
102
    let mut parser = (
2✔
103
        delimited(map_err(tag("\"")), parse_regex_path, map_err(tag("\""))),
2✔
104
        map_err(whitespace(tag(","))),
2✔
105
        map_err(parse_version),
2✔
106
        map_err(whitespace(tag(","))),
2✔
107
        map_err(ComparisonOperator::parse),
2✔
108
    );
2✔
109

110
    let (remaining_input, ((path, regex), _, version, _, comparator)) = parser.parse(input)?;
2✔
111

112
    if regex.captures_len() != 2 {
2✔
113
        return Err(Err::Failure(
1✔
114
            ParsingErrorKind::InvalidRegexUnknown.at(input),
1✔
115
        ));
1✔
116
    }
1✔
117

1✔
118
    Ok((remaining_input, (path, regex, version, comparator)))
1✔
119
}
2✔
120

121
fn parse_description_contains_args(input: &str) -> ParsingResult<(PathBuf, Regex)> {
1✔
122
    let mut parser = (
1✔
123
        map_err(parse_path),
1✔
124
        map_err(whitespace(tag(","))),
1✔
125
        delimited(
1✔
126
            map_err(tag("\"")),
1✔
127
            map_parser(is_not("\""), parse_regex),
1✔
128
            map_err(tag("\"")),
1✔
129
        ),
1✔
130
    );
1✔
131

132
    let (remaining_input, (path, _, regex)) = parser.parse(input)?;
1✔
133

134
    Ok((remaining_input, (path, regex)))
1✔
135
}
1✔
136

137
fn parse_crc(input: &str) -> ParsingResult<u32> {
12✔
138
    u32::from_str_radix(input, 16)
12✔
139
        .map(|c| ("", c))
12✔
140
        .map_err(|e| Err::Failure(ParsingErrorKind::from(e).at(input)))
12✔
141
}
12✔
142

143
fn parse_checksum_args(input: &str) -> ParsingResult<(PathBuf, u32)> {
12✔
144
    let mut parser = (
12✔
145
        map_err(parse_path),
12✔
146
        map_err(whitespace(tag(","))),
12✔
147
        map_parser(hex_digit1, parse_crc),
12✔
148
    );
12✔
149

150
    let (remaining_input, (path, _, crc)) = parser.parse(input)?;
12✔
151

152
    Ok((remaining_input, (path, crc)))
11✔
153
}
12✔
154

155
fn parse_non_regex_path(input: &str) -> ParsingResult<PathBuf> {
29✔
156
    let (remaining_input, path) = map(is_not(INVALID_NON_REGEX_PATH_CHARS), |path: &str| {
29✔
157
        PathBuf::from(path)
29✔
158
    })
29✔
159
    .parse(input)?;
29✔
160

161
    Ok((remaining_input, path))
29✔
162
}
29✔
163

164
/// Parse a string that is a path where the last component is a regex string
165
/// that may contain characters that are invalid in paths but valid in regex.
166
fn parse_regex_path(input: &str) -> ParsingResult<(PathBuf, Regex)> {
12✔
167
    let (remaining_input, string) = is_not(INVALID_REGEX_PATH_CHARS)(input)?;
12✔
168

169
    if string.ends_with('/') {
12✔
170
        return Err(Err::Failure(
3✔
171
            ParsingErrorKind::PathEndsInADirectorySeparator(string.into()).at(input),
3✔
172
        ));
3✔
173
    }
9✔
174

9✔
175
    let (parent_path_slice, regex_slice) = string.rsplit_once('/').unwrap_or((".", string));
9✔
176

9✔
177
    let parent_path = PathBuf::from(parent_path_slice);
9✔
178

179
    let regex = parse_anchored_regex(regex_slice)?.1;
9✔
180

181
    Ok((remaining_input, (parent_path, regex)))
8✔
182
}
12✔
183

184
fn parse_regex_filename(input: &str) -> ParsingResult<Regex> {
2✔
185
    map_parser(is_not(INVALID_REGEX_PATH_CHARS), parse_anchored_regex).parse(input)
2✔
186
}
2✔
187

188
impl Function {
189
    #[expect(clippy::too_many_lines)]
190
    pub fn parse(input: &str) -> ParsingResult<Function> {
79✔
191
        alt((
79✔
192
            map(
79✔
193
                delimited(
79✔
194
                    map_err(tag("file(\"")),
79✔
195
                    parse_non_regex_path,
79✔
196
                    map_err(tag("\")")),
79✔
197
                ),
79✔
198
                Function::FilePath,
79✔
199
            ),
79✔
200
            map(
79✔
201
                delimited(
79✔
202
                    map_err(tag("file(\"")),
79✔
203
                    parse_regex_path,
79✔
204
                    map_err(tag("\")")),
79✔
205
                ),
79✔
206
                |(path, regex)| Function::FileRegex(path, regex),
79✔
207
            ),
79✔
208
            map(
79✔
209
                delimited(
79✔
210
                    map_err(tag("file_size(")),
79✔
211
                    parse_file_size_args,
79✔
212
                    map_err(tag(")")),
79✔
213
                ),
79✔
214
                |(path, size)| Function::FileSize(path, size),
79✔
215
            ),
79✔
216
            map(
79✔
217
                delimited(
79✔
218
                    map_err(tag("readable(\"")),
79✔
219
                    parse_non_regex_path,
79✔
220
                    map_err(tag("\")")),
79✔
221
                ),
79✔
222
                Function::Readable,
79✔
223
            ),
79✔
224
            map(
79✔
225
                delimited(
79✔
226
                    map_err(tag("is_executable(\"")),
79✔
227
                    parse_non_regex_path,
79✔
228
                    map_err(tag("\")")),
79✔
229
                ),
79✔
230
                Function::IsExecutable,
79✔
231
            ),
79✔
232
            map(
79✔
233
                delimited(
79✔
234
                    map_err(tag("active(\"")),
79✔
235
                    parse_non_regex_path,
79✔
236
                    map_err(tag("\")")),
79✔
237
                ),
79✔
238
                Function::ActivePath,
79✔
239
            ),
79✔
240
            map(
79✔
241
                delimited(
79✔
242
                    map_err(tag("active(\"")),
79✔
243
                    parse_regex_filename,
79✔
244
                    map_err(tag("\")")),
79✔
245
                ),
79✔
246
                Function::ActiveRegex,
79✔
247
            ),
79✔
248
            map(
79✔
249
                delimited(
79✔
250
                    map_err(tag("is_master(\"")),
79✔
251
                    parse_non_regex_path,
79✔
252
                    map_err(tag("\")")),
79✔
253
                ),
79✔
254
                Function::IsMaster,
79✔
255
            ),
79✔
256
            map(
79✔
257
                delimited(
79✔
258
                    map_err(tag("many(\"")),
79✔
259
                    parse_regex_path,
79✔
260
                    map_err(tag("\")")),
79✔
261
                ),
79✔
262
                |(path, regex)| Function::Many(path, regex),
79✔
263
            ),
79✔
264
            map(
79✔
265
                delimited(
79✔
266
                    map_err(tag("many_active(\"")),
79✔
267
                    parse_regex_filename,
79✔
268
                    map_err(tag("\")")),
79✔
269
                ),
79✔
270
                Function::ManyActive,
79✔
271
            ),
79✔
272
            map(
79✔
273
                delimited(
79✔
274
                    map_err(tag("version(")),
79✔
275
                    parse_version_args,
79✔
276
                    map_err(tag(")")),
79✔
277
                ),
79✔
278
                |(path, version, comparator)| Function::Version(path, version, comparator),
79✔
279
            ),
79✔
280
            map(
79✔
281
                delimited(
79✔
282
                    map_err(tag("product_version(")),
79✔
283
                    parse_version_args,
79✔
284
                    map_err(tag(")")),
79✔
285
                ),
79✔
286
                |(path, version, comparator)| Function::ProductVersion(path, version, comparator),
79✔
287
            ),
79✔
288
            map(
79✔
289
                delimited(
79✔
290
                    map_err(tag("filename_version(")),
79✔
291
                    parse_filename_version_args,
79✔
292
                    map_err(tag(")")),
79✔
293
                ),
79✔
294
                |(path, regex, version, comparator)| {
79✔
295
                    Function::FilenameVersion(path, regex, version, comparator)
1✔
296
                },
79✔
297
            ),
79✔
298
            map(
79✔
299
                delimited(
79✔
300
                    map_err(tag("checksum(")),
79✔
301
                    parse_checksum_args,
79✔
302
                    map_err(tag(")")),
79✔
303
                ),
79✔
304
                |(path, crc)| Function::Checksum(path, crc),
79✔
305
            ),
79✔
306
            map(
79✔
307
                delimited(
79✔
308
                    map_err(tag("description_contains(")),
79✔
309
                    parse_description_contains_args,
79✔
310
                    map_err(tag(")")),
79✔
311
                ),
79✔
312
                |(path, regex)| Function::DescriptionContains(path, regex),
79✔
313
            ),
79✔
314
        ))
79✔
315
        .parse(input)
79✔
316
    }
79✔
317
}
318

319
#[cfg(test)]
320
mod tests {
321
    use std::path::Path;
322

323
    use super::*;
324

325
    #[test]
326
    fn parse_regex_should_produce_case_insensitive_regex() {
1✔
327
        let (_, regex) = parse_regex("cargo.*").unwrap();
1✔
328

1✔
329
        assert!(regex.is_match("Cargo.toml"));
1✔
330
    }
1✔
331

332
    #[test]
333
    fn parse_regex_should_produce_a_regex_that_does_partially_match() {
1✔
334
        let (_, regex) = parse_regex("argo.").unwrap();
1✔
335

1✔
336
        assert!(regex.is_match("Cargo.toml"));
1✔
337
    }
1✔
338

339
    #[test]
340
    fn parse_anchored_regex_should_produce_case_insensitive_regex() {
1✔
341
        let (_, regex) = parse_anchored_regex("cargo.*").unwrap();
1✔
342

1✔
343
        assert!(regex.is_match("Cargo.toml"));
1✔
344
    }
1✔
345

346
    #[test]
347
    fn parse_anchored_regex_should_produce_a_regex_that_does_not_partially_match() {
1✔
348
        let (_, regex) = parse_anchored_regex("cargo.").unwrap();
1✔
349

1✔
350
        assert!(!regex.is_match("Cargo.toml"));
1✔
351
    }
1✔
352

353
    #[test]
354
    fn function_parse_should_parse_a_file_path_function() {
1✔
355
        let output = Function::parse("file(\"Cargo.toml\")").unwrap();
1✔
356

1✔
357
        assert!(output.0.is_empty());
1✔
358
        match output.1 {
1✔
359
            Function::FilePath(f) => assert_eq!(Path::new("Cargo.toml"), f),
1✔
360
            _ => panic!("Expected a file path function"),
×
361
        }
362
    }
1✔
363

364
    #[test]
365
    fn function_parse_should_parse_a_file_regex_function_with_no_parent_path() {
1✔
366
        let output = Function::parse("file(\"Cargo.*\")").unwrap();
1✔
367

1✔
368
        assert!(output.0.is_empty());
1✔
369
        match output.1 {
1✔
370
            Function::FileRegex(p, r) => {
1✔
371
                assert_eq!(PathBuf::from("."), p);
1✔
372
                assert_eq!(Regex::new("^Cargo.*$").unwrap().as_str(), r.as_str());
1✔
373
            }
374
            _ => panic!("Expected a file regex function"),
×
375
        }
376
    }
1✔
377

378
    #[test]
379
    fn function_parse_should_parse_a_file_regex_function_with_a_parent_path() {
1✔
380
        let output = Function::parse("file(\"subdir/Cargo.*\")").unwrap();
1✔
381

1✔
382
        assert!(output.0.is_empty());
1✔
383
        match output.1 {
1✔
384
            Function::FileRegex(p, r) => {
1✔
385
                assert_eq!(PathBuf::from("subdir"), p);
1✔
386
                assert_eq!(Regex::new("^Cargo.*$").unwrap().as_str(), r.as_str());
1✔
387
            }
388
            _ => panic!("Expected a file regex function"),
×
389
        }
390
    }
1✔
391

392
    #[test]
393
    fn function_parse_should_error_if_given_a_file_regex_function_ending_in_a_forward_slash() {
1✔
394
        assert!(Function::parse("file(\"sub\\dir/\")").is_err());
1✔
395
    }
1✔
396

397
    #[test]
398
    fn function_parse_should_parse_a_file_size_function() {
1✔
399
        let output = Function::parse("file_size(\"Cargo.toml\", 1234)").unwrap();
1✔
400

1✔
401
        assert!(output.0.is_empty());
1✔
402
        match output.1 {
1✔
403
            Function::FileSize(f, s) => {
1✔
404
                assert_eq!(Path::new("Cargo.toml"), f);
1✔
405
                assert_eq!(1234, s);
1✔
406
            }
407
            _ => panic!("Expected a file size function"),
×
408
        }
409
    }
1✔
410

411
    #[test]
412
    fn function_parse_should_parse_a_readable_function() {
1✔
413
        let output = Function::parse("readable(\"Cargo.toml\")").unwrap();
1✔
414

1✔
415
        assert!(output.0.is_empty());
1✔
416
        match output.1 {
1✔
417
            Function::Readable(f) => assert_eq!(Path::new("Cargo.toml"), f),
1✔
418
            _ => panic!("Expected a readable function"),
×
419
        }
420
    }
1✔
421

422
    #[test]
423
    fn function_parse_should_parse_an_is_executable_function() {
1✔
424
        let output = Function::parse("is_executable(\"Cargo.toml\")").unwrap();
1✔
425

1✔
426
        assert!(output.0.is_empty());
1✔
427
        match output.1 {
1✔
428
            Function::IsExecutable(f) => assert_eq!(Path::new("Cargo.toml"), f),
1✔
429
            _ => panic!("Expected an is_executable function"),
×
430
        }
431
    }
1✔
432

433
    #[test]
434
    fn function_parse_should_parse_an_active_path_function() {
1✔
435
        let output = Function::parse("active(\"Cargo.toml\")").unwrap();
1✔
436

1✔
437
        assert!(output.0.is_empty());
1✔
438
        match output.1 {
1✔
439
            Function::ActivePath(f) => assert_eq!(Path::new("Cargo.toml"), f),
1✔
440
            _ => panic!("Expected an active path function"),
×
441
        }
442
    }
1✔
443

444
    #[test]
445
    fn function_parse_should_parse_an_active_regex_function() {
1✔
446
        let output = Function::parse("active(\"Cargo.*\")").unwrap();
1✔
447

1✔
448
        assert!(output.0.is_empty());
1✔
449
        match output.1 {
1✔
450
            Function::ActiveRegex(r) => {
1✔
451
                assert_eq!(Regex::new("^Cargo.*$").unwrap().as_str(), r.as_str());
1✔
452
            }
453
            _ => panic!("Expected an active regex function"),
×
454
        }
455
    }
1✔
456

457
    #[test]
458
    fn function_parse_should_parse_an_is_master_function() {
1✔
459
        let output = Function::parse("is_master(\"Blank.esm\")").unwrap();
1✔
460

1✔
461
        assert!(output.0.is_empty());
1✔
462
        match output.1 {
1✔
463
            Function::IsMaster(f) => assert_eq!(Path::new("Blank.esm"), f),
1✔
464
            _ => panic!("Expected an is master function"),
×
465
        }
466
    }
1✔
467

468
    #[test]
469
    fn function_parse_should_parse_a_many_function_with_no_parent_path() {
1✔
470
        let output = Function::parse("many(\"Cargo.*\")").unwrap();
1✔
471

1✔
472
        assert!(output.0.is_empty());
1✔
473
        match output.1 {
1✔
474
            Function::Many(p, r) => {
1✔
475
                assert_eq!(PathBuf::from("."), p);
1✔
476
                assert_eq!(Regex::new("^Cargo.*$").unwrap().as_str(), r.as_str());
1✔
477
            }
478
            _ => panic!("Expected a many function"),
×
479
        }
480
    }
1✔
481

482
    #[test]
483
    fn function_parse_should_parse_a_many_function_with_a_parent_path() {
1✔
484
        let output = Function::parse("many(\"subdir/Cargo.*\")").unwrap();
1✔
485

1✔
486
        assert!(output.0.is_empty());
1✔
487
        match output.1 {
1✔
488
            Function::Many(p, r) => {
1✔
489
                assert_eq!(PathBuf::from("subdir"), p);
1✔
490
                assert_eq!(Regex::new("^Cargo.*$").unwrap().as_str(), r.as_str());
1✔
491
            }
492
            _ => panic!("Expected a many function"),
×
493
        }
494
    }
1✔
495

496
    #[test]
497
    fn function_parse_should_error_if_given_a_many_function_ending_in_a_forward_slash() {
1✔
498
        assert!(Function::parse("many(\"subdir/\")").is_err());
1✔
499
    }
1✔
500

501
    #[test]
502
    fn function_parse_should_parse_a_many_active_function() {
1✔
503
        let output = Function::parse("many_active(\"Cargo.*\")").unwrap();
1✔
504

1✔
505
        assert!(output.0.is_empty());
1✔
506
        match output.1 {
1✔
507
            Function::ManyActive(r) => {
1✔
508
                assert_eq!(Regex::new("^Cargo.*$").unwrap().as_str(), r.as_str());
1✔
509
            }
510
            _ => panic!("Expected a many active function"),
×
511
        }
512
    }
1✔
513

514
    #[test]
515
    fn function_parse_should_parse_a_checksum_function() {
1✔
516
        let output = Function::parse("checksum(\"Cargo.toml\", DEADBEEF)").unwrap();
1✔
517

1✔
518
        assert!(output.0.is_empty());
1✔
519
        match output.1 {
1✔
520
            Function::Checksum(path, crc) => {
1✔
521
                assert_eq!(Path::new("Cargo.toml"), path);
1✔
522
                assert_eq!(0xDEAD_BEEF, crc);
1✔
523
            }
524
            _ => panic!("Expected a checksum function"),
×
525
        }
526
    }
1✔
527

528
    #[test]
529
    fn function_parse_should_parse_a_version_equals_function() {
1✔
530
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", ==)").unwrap();
1✔
531

1✔
532
        assert!(output.0.is_empty());
1✔
533
        match output.1 {
1✔
534
            Function::Version(path, version, comparator) => {
1✔
535
                assert_eq!(Path::new("Cargo.toml"), path);
1✔
536
                assert_eq!("1.2", version);
1✔
537
                assert_eq!(ComparisonOperator::Equal, comparator);
1✔
538
            }
539
            _ => panic!("Expected a version function"),
×
540
        }
541
    }
1✔
542

543
    #[test]
544
    fn function_parse_should_parse_a_version_not_equals_function() {
1✔
545
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", !=)").unwrap();
1✔
546

1✔
547
        assert!(output.0.is_empty());
1✔
548
        match output.1 {
1✔
549
            Function::Version(path, version, comparator) => {
1✔
550
                assert_eq!(Path::new("Cargo.toml"), path);
1✔
551
                assert_eq!("1.2", version);
1✔
552
                assert_eq!(ComparisonOperator::NotEqual, comparator);
1✔
553
            }
554
            _ => panic!("Expected a version function"),
×
555
        }
556
    }
1✔
557

558
    #[test]
559
    fn function_parse_should_parse_a_version_less_than_function() {
1✔
560
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", <)").unwrap();
1✔
561

1✔
562
        assert!(output.0.is_empty());
1✔
563
        match output.1 {
1✔
564
            Function::Version(path, version, comparator) => {
1✔
565
                assert_eq!(Path::new("Cargo.toml"), path);
1✔
566
                assert_eq!("1.2", version);
1✔
567
                assert_eq!(ComparisonOperator::LessThan, comparator);
1✔
568
            }
569
            _ => panic!("Expected a version function"),
×
570
        }
571
    }
1✔
572

573
    #[test]
574
    fn function_parse_should_parse_a_version_greater_than_function() {
1✔
575
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", >)").unwrap();
1✔
576

1✔
577
        assert!(output.0.is_empty());
1✔
578
        match output.1 {
1✔
579
            Function::Version(path, version, comparator) => {
1✔
580
                assert_eq!(Path::new("Cargo.toml"), path);
1✔
581
                assert_eq!("1.2", version);
1✔
582
                assert_eq!(ComparisonOperator::GreaterThan, comparator);
1✔
583
            }
584
            _ => panic!("Expected a version function"),
×
585
        }
586
    }
1✔
587

588
    #[test]
589
    fn function_parse_should_parse_a_version_less_than_or_equal_to_function() {
1✔
590
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", <=)").unwrap();
1✔
591

1✔
592
        assert!(output.0.is_empty());
1✔
593
        match output.1 {
1✔
594
            Function::Version(path, version, comparator) => {
1✔
595
                assert_eq!(Path::new("Cargo.toml"), path);
1✔
596
                assert_eq!("1.2", version);
1✔
597
                assert_eq!(ComparisonOperator::LessThanOrEqual, comparator);
1✔
598
            }
599
            _ => panic!("Expected a version function"),
×
600
        }
601
    }
1✔
602

603
    #[test]
604
    fn function_parse_should_parse_a_version_greater_than_or_equal_to_function() {
1✔
605
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", >=)").unwrap();
1✔
606

1✔
607
        assert!(output.0.is_empty());
1✔
608
        match output.1 {
1✔
609
            Function::Version(path, version, comparator) => {
1✔
610
                assert_eq!(Path::new("Cargo.toml"), path);
1✔
611
                assert_eq!("1.2", version);
1✔
612
                assert_eq!(ComparisonOperator::GreaterThanOrEqual, comparator);
1✔
613
            }
614
            _ => panic!("Expected a version function"),
×
615
        }
616
    }
1✔
617

618
    #[test]
619
    fn function_parse_should_parse_a_version_with_a_path_containing_backslashes() {
1✔
620
        let output = Function::parse("version(\"..\\Cargo.toml\", \"1.2\", ==)").unwrap();
1✔
621

1✔
622
        assert!(output.0.is_empty());
1✔
623
        match output.1 {
1✔
624
            Function::Version(path, version, comparator) => {
1✔
625
                assert_eq!(Path::new("..\\Cargo.toml"), path);
1✔
626
                assert_eq!("1.2", version);
1✔
627
                assert_eq!(ComparisonOperator::Equal, comparator);
1✔
628
            }
629
            _ => panic!("Expected a version function"),
×
630
        }
631
    }
1✔
632

633
    #[test]
634
    fn function_parse_should_parse_a_product_version_equals_function() {
1✔
635
        let output = Function::parse("product_version(\"Cargo.toml\", \"1.2\", ==)").unwrap();
1✔
636

1✔
637
        assert!(output.0.is_empty());
1✔
638
        match output.1 {
1✔
639
            Function::ProductVersion(path, version, comparator) => {
1✔
640
                assert_eq!(Path::new("Cargo.toml"), path);
1✔
641
                assert_eq!("1.2", version);
1✔
642
                assert_eq!(ComparisonOperator::Equal, comparator);
1✔
643
            }
644
            _ => panic!("Expected a product version function"),
×
645
        }
646
    }
1✔
647

648
    #[test]
649
    fn function_parse_should_parse_a_filename_version_equals_function() {
1✔
650
        let output =
1✔
651
            Function::parse("filename_version(\"subdir/Cargo (.+).toml\", \"1.2\", ==)").unwrap();
1✔
652

1✔
653
        assert!(output.0.is_empty());
1✔
654
        match output.1 {
1✔
655
            Function::FilenameVersion(path, regex, version, comparator) => {
1✔
656
                assert_eq!(PathBuf::from("subdir"), path);
1✔
657
                assert_eq!(
1✔
658
                    Regex::new("^Cargo (.+).toml$").unwrap().as_str(),
1✔
659
                    regex.as_str()
1✔
660
                );
1✔
661
                assert_eq!("1.2", version);
1✔
662
                assert_eq!(ComparisonOperator::Equal, comparator);
1✔
663
            }
664
            _ => panic!("Expected a filename version function"),
×
665
        }
666
    }
1✔
667

668
    #[test]
669
    fn function_parse_should_error_if_the_filename_version_regex_does_not_contain_an_explicit_capture_group(
1✔
670
    ) {
1✔
671
        assert!(
1✔
672
            Function::parse("filename_version(\"subdir/Cargo .+.toml\", \"1.2\", ==)").is_err()
1✔
673
        );
1✔
674
    }
1✔
675

676
    #[test]
677
    fn function_parse_should_parse_a_description_contains_function() {
1✔
678
        let lowercase_non_ascii = "\u{20ac}\u{192}.";
1✔
679
        let function = format!("description_contains(\"Blank.esp\", \"{lowercase_non_ascii}\")");
1✔
680
        let output = Function::parse(&function).unwrap();
1✔
681

1✔
682
        assert!(output.0.is_empty());
1✔
683
        match output.1 {
1✔
684
            Function::DescriptionContains(p, r) => {
1✔
685
                assert_eq!(PathBuf::from("Blank.esp"), p);
1✔
686
                assert_eq!(
1✔
687
                    Regex::new(lowercase_non_ascii).unwrap().as_str(),
1✔
688
                    r.as_str()
1✔
689
                );
1✔
690
            }
691
            _ => panic!("Expected a description_contains function"),
×
692
        }
693
    }
1✔
694
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc