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

loot / loot-condition-interpreter / 21119149748

18 Jan 2026 05:02PM UTC coverage: 90.27% (+0.05%) from 90.219%
21119149748

push

github

Ortham
Write new *version condition syntax

3 of 3 new or added lines in 1 file covered. (100.0%)

24 existing lines in 1 file now uncovered.

4546 of 5036 relevant lines covered (90.27%)

31.86 hits per line

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

94.79
/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> {
38✔
19
        alt((
38✔
20
            value(ComparisonOperator::Equal, tag("==")),
38✔
21
            value(ComparisonOperator::NotEqual, tag("!=")),
38✔
22
            value(ComparisonOperator::LessThanOrEqual, tag("<=")),
38✔
23
            value(ComparisonOperator::GreaterThanOrEqual, tag(">=")),
38✔
24
            value(ComparisonOperator::LessThan, tag("<")),
38✔
25
            value(ComparisonOperator::GreaterThan, tag(">")),
38✔
26
        ))
38✔
27
        .parse(input)
38✔
28
    }
38✔
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> {
36✔
36
    RegexBuilder::new(input)
36✔
37
        .case_insensitive(true)
36✔
38
        .build()
36✔
39
        .map(|r| ("", r))
36✔
40
}
36✔
41

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

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

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

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

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

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

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

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

85
fn parse_version_args(input: &str) -> ParsingResult<'_, (PathBuf, String, ComparisonOperator)> {
32✔
86
    let parser = alt((
32✔
87
        map(
32✔
88
            (
32✔
89
                parse_path,
32✔
90
                whitespace(tag(",")),
32✔
91
                parse_version,
32✔
92
                whitespace(tag(",")),
32✔
93
                ComparisonOperator::parse,
32✔
94
            ),
32✔
95
            |(path, _, version, _, comparator)| (path, version, comparator),
28✔
96
        ),
97
        map(
32✔
98
            (
32✔
99
                parse_path,
32✔
100
                whitespace(tag(",")),
32✔
101
                ComparisonOperator::parse,
32✔
102
                whitespace(tag(",")),
32✔
103
                parse_version,
32✔
104
            ),
32✔
105
            |(path, _, comparator, _, version)| (path, version, comparator),
4✔
106
        ),
107
    ));
108

109
    map_err(parser).parse(input)
32✔
110
}
32✔
111

112
fn parse_filename_version_args(
6✔
113
    input: &str,
6✔
114
) -> ParsingResult<'_, (PathBuf, Regex, String, ComparisonOperator)> {
6✔
115
    let mut parser = alt((
6✔
116
        map(
6✔
117
            (
6✔
118
                delimited(map_err(tag("\"")), parse_regex_path, map_err(tag("\""))),
6✔
119
                map_err(whitespace(tag(","))),
6✔
120
                map_err(parse_version),
6✔
121
                map_err(whitespace(tag(","))),
6✔
122
                map_err(ComparisonOperator::parse),
6✔
123
            ),
6✔
124
            |((path, regex), _, version, _, comparator)| (path, regex, version, comparator),
4✔
125
        ),
126
        map(
6✔
127
            (
6✔
128
                delimited(map_err(tag("\"")), parse_regex_path, map_err(tag("\""))),
6✔
129
                map_err(whitespace(tag(","))),
6✔
130
                map_err(ComparisonOperator::parse),
6✔
131
                map_err(whitespace(tag(","))),
6✔
132
                map_err(parse_version),
6✔
133
            ),
6✔
134
            |((path, regex), _, comparator, _, version)| (path, regex, version, comparator),
2✔
135
        ),
136
    ));
137

138
    let (remaining_input, (path, regex, version, comparator)) = parser.parse(input)?;
6✔
139

140
    if regex.captures_len() != 2 {
6✔
141
        return Err(Err::Failure(
2✔
142
            ParsingErrorKind::InvalidRegexUnknown.at(input),
2✔
143
        ));
2✔
144
    }
4✔
145

146
    Ok((remaining_input, (path, regex, version, comparator)))
4✔
147
}
6✔
148

149
fn parse_description_contains_args(input: &str) -> ParsingResult<'_, (PathBuf, Regex)> {
2✔
150
    let mut parser = (
2✔
151
        map_err(parse_path),
2✔
152
        map_err(whitespace(tag(","))),
2✔
153
        delimited(
2✔
154
            map_err(tag("\"")),
2✔
155
            map_parser(is_not("\""), parse_regex),
2✔
156
            map_err(tag("\"")),
2✔
157
        ),
2✔
158
    );
2✔
159

160
    let (remaining_input, (path, _, regex)) = parser.parse(input)?;
2✔
161

162
    Ok((remaining_input, (path, regex)))
2✔
163
}
2✔
164

165
fn parse_crc(input: &str) -> ParsingResult<'_, u32> {
24✔
166
    u32::from_str_radix(input, 16)
24✔
167
        .map(|c| ("", c))
24✔
168
        .map_err(|e| Err::Failure(ParsingErrorKind::from(e).at(input)))
24✔
169
}
24✔
170

171
fn parse_checksum_args(input: &str) -> ParsingResult<'_, (PathBuf, u32)> {
24✔
172
    let mut parser = (
24✔
173
        map_err(parse_path),
24✔
174
        map_err(whitespace(tag(","))),
24✔
175
        map_parser(hex_digit1, parse_crc),
24✔
176
    );
24✔
177

178
    let (remaining_input, (path, _, crc)) = parser.parse(input)?;
24✔
179

180
    Ok((remaining_input, (path, crc)))
22✔
181
}
24✔
182

183
fn parse_non_regex_path(input: &str) -> ParsingResult<'_, PathBuf> {
58✔
184
    let (remaining_input, path) = map(is_not(INVALID_NON_REGEX_PATH_CHARS), |path: &str| {
58✔
185
        PathBuf::from(path)
58✔
186
    })
58✔
187
    .parse(input)?;
58✔
188

189
    Ok((remaining_input, path))
58✔
190
}
58✔
191

192
/// Parse a string that is a path where the last component is a regex string
193
/// that may contain characters that are invalid in paths but valid in regex.
194
fn parse_regex_path(input: &str) -> ParsingResult<'_, (PathBuf, Regex)> {
28✔
195
    let (remaining_input, string) = is_not(INVALID_REGEX_PATH_CHARS)(input)?;
28✔
196

197
    if string.ends_with('/') {
28✔
198
        return Err(Err::Failure(
6✔
199
            ParsingErrorKind::PathEndsInADirectorySeparator(string.into()).at(input),
6✔
200
        ));
6✔
201
    }
22✔
202

203
    let (parent_path_slice, regex_slice) = string.rsplit_once('/').unwrap_or((".", string));
22✔
204

205
    let parent_path = PathBuf::from(parent_path_slice);
22✔
206

207
    let regex = parse_anchored_regex(regex_slice)?.1;
22✔
208

209
    Ok((remaining_input, (parent_path, regex)))
20✔
210
}
28✔
211

212
fn parse_regex_filename(input: &str) -> ParsingResult<'_, Regex> {
4✔
213
    map_parser(is_not(INVALID_REGEX_PATH_CHARS), parse_anchored_regex).parse(input)
4✔
214
}
4✔
215

216
impl Function {
217
    #[expect(clippy::too_many_lines)]
218
    pub fn parse(input: &str) -> ParsingResult<'_, Function> {
164✔
219
        alt((
164✔
220
            map(
164✔
221
                delimited(
164✔
222
                    map_err(tag("file(\"")),
164✔
223
                    parse_non_regex_path,
224
                    map_err(tag("\")")),
164✔
225
                ),
226
                Function::FilePath,
227
            ),
228
            map(
164✔
229
                delimited(
164✔
230
                    map_err(tag("file(\"")),
164✔
231
                    parse_regex_path,
232
                    map_err(tag("\")")),
164✔
233
                ),
234
                |(path, regex)| Function::FileRegex(path, regex),
4✔
235
            ),
236
            map(
164✔
237
                delimited(
164✔
238
                    map_err(tag("file_size(")),
164✔
239
                    parse_file_size_args,
240
                    map_err(tag(")")),
164✔
241
                ),
242
                |(path, size)| Function::FileSize(path, size),
2✔
243
            ),
244
            map(
164✔
245
                delimited(
164✔
246
                    map_err(tag("readable(\"")),
164✔
247
                    parse_non_regex_path,
248
                    map_err(tag("\")")),
164✔
249
                ),
250
                Function::Readable,
251
            ),
252
            map(
164✔
253
                delimited(
164✔
254
                    map_err(tag("is_executable(\"")),
164✔
255
                    parse_non_regex_path,
256
                    map_err(tag("\")")),
164✔
257
                ),
258
                Function::IsExecutable,
259
            ),
260
            map(
164✔
261
                delimited(
164✔
262
                    map_err(tag("active(\"")),
164✔
263
                    parse_non_regex_path,
264
                    map_err(tag("\")")),
164✔
265
                ),
266
                Function::ActivePath,
267
            ),
268
            map(
164✔
269
                delimited(
164✔
270
                    map_err(tag("active(\"")),
164✔
271
                    parse_regex_filename,
272
                    map_err(tag("\")")),
164✔
273
                ),
274
                Function::ActiveRegex,
275
            ),
276
            map(
164✔
277
                delimited(
164✔
278
                    map_err(tag("is_master(\"")),
164✔
279
                    parse_non_regex_path,
280
                    map_err(tag("\")")),
164✔
281
                ),
282
                Function::IsMaster,
283
            ),
284
            map(
164✔
285
                delimited(
164✔
286
                    map_err(tag("many(\"")),
164✔
287
                    parse_regex_path,
288
                    map_err(tag("\")")),
164✔
289
                ),
290
                |(path, regex)| Function::Many(path, regex),
6✔
291
            ),
292
            map(
164✔
293
                delimited(
164✔
294
                    map_err(tag("many_active(\"")),
164✔
295
                    parse_regex_filename,
296
                    map_err(tag("\")")),
164✔
297
                ),
298
                Function::ManyActive,
299
            ),
300
            map(
164✔
301
                delimited(
164✔
302
                    map_err(tag("version(")),
164✔
303
                    parse_version_args,
304
                    map_err(tag(")")),
164✔
305
                ),
306
                |(path, version, comparator)| Function::Version(path, version, comparator),
28✔
307
            ),
308
            map(
164✔
309
                delimited(
164✔
310
                    map_err(tag("product_version(")),
164✔
311
                    parse_version_args,
312
                    map_err(tag(")")),
164✔
313
                ),
314
                |(path, version, comparator)| Function::ProductVersion(path, version, comparator),
4✔
315
            ),
316
            map(
164✔
317
                delimited(
164✔
318
                    map_err(tag("filename_version(")),
164✔
319
                    parse_filename_version_args,
320
                    map_err(tag(")")),
164✔
321
                ),
322
                |(path, regex, version, comparator)| {
4✔
323
                    Function::FilenameVersion(path, regex, version, comparator)
4✔
324
                },
4✔
325
            ),
326
            map(
164✔
327
                delimited(
164✔
328
                    map_err(tag("checksum(")),
164✔
329
                    parse_checksum_args,
330
                    map_err(tag(")")),
164✔
331
                ),
332
                |(path, crc)| Function::Checksum(path, crc),
22✔
333
            ),
334
            map(
164✔
335
                delimited(
164✔
336
                    map_err(tag("description_contains(")),
164✔
337
                    parse_description_contains_args,
338
                    map_err(tag(")")),
164✔
339
                ),
340
                |(path, regex)| Function::DescriptionContains(path, regex),
2✔
341
            ),
342
        ))
343
        .parse(input)
164✔
344
    }
164✔
345
}
346

347
#[cfg(test)]
348
mod tests {
349
    use std::path::Path;
350

351
    use super::*;
352

353
    #[test]
354
    fn parse_regex_should_produce_case_insensitive_regex() {
2✔
355
        let (_, regex) = parse_regex("cargo.*").unwrap();
2✔
356

357
        assert!(regex.is_match("Cargo.toml"));
2✔
358
    }
2✔
359

360
    #[test]
361
    fn parse_regex_should_produce_a_regex_that_does_partially_match() {
2✔
362
        let (_, regex) = parse_regex("argo.").unwrap();
2✔
363

364
        assert!(regex.is_match("Cargo.toml"));
2✔
365
    }
2✔
366

367
    #[test]
368
    fn parse_anchored_regex_should_produce_case_insensitive_regex() {
2✔
369
        let (_, regex) = parse_anchored_regex("cargo.*").unwrap();
2✔
370

371
        assert!(regex.is_match("Cargo.toml"));
2✔
372
    }
2✔
373

374
    #[test]
375
    fn parse_anchored_regex_should_produce_a_regex_that_does_not_partially_match() {
2✔
376
        let (_, regex) = parse_anchored_regex("cargo.").unwrap();
2✔
377

378
        assert!(!regex.is_match("Cargo.toml"));
2✔
379
    }
2✔
380

381
    #[test]
382
    fn function_parse_should_parse_a_file_path_function() {
2✔
383
        let output = Function::parse("file(\"Cargo.toml\")").unwrap();
2✔
384

385
        assert!(output.0.is_empty());
2✔
386
        match output.1 {
2✔
387
            Function::FilePath(f) => assert_eq!(Path::new("Cargo.toml"), f),
2✔
388
            _ => panic!("Expected a file path function"),
×
389
        }
390
    }
2✔
391

392
    #[test]
393
    fn function_parse_should_parse_a_file_regex_function_with_no_parent_path() {
2✔
394
        let output = Function::parse("file(\"Cargo.*\")").unwrap();
2✔
395

396
        assert!(output.0.is_empty());
2✔
397
        match output.1 {
2✔
398
            Function::FileRegex(p, r) => {
2✔
399
                assert_eq!(PathBuf::from("."), p);
2✔
400
                assert_eq!(Regex::new("^Cargo.*$").unwrap().as_str(), r.as_str());
2✔
401
            }
UNCOV
402
            _ => panic!("Expected a file regex function"),
×
403
        }
404
    }
2✔
405

406
    #[test]
407
    fn function_parse_should_parse_a_file_regex_function_with_a_parent_path() {
2✔
408
        let output = Function::parse("file(\"subdir/Cargo.*\")").unwrap();
2✔
409

410
        assert!(output.0.is_empty());
2✔
411
        match output.1 {
2✔
412
            Function::FileRegex(p, r) => {
2✔
413
                assert_eq!(PathBuf::from("subdir"), p);
2✔
414
                assert_eq!(Regex::new("^Cargo.*$").unwrap().as_str(), r.as_str());
2✔
415
            }
UNCOV
416
            _ => panic!("Expected a file regex function"),
×
417
        }
418
    }
2✔
419

420
    #[test]
421
    fn function_parse_should_error_if_given_a_file_regex_function_ending_in_a_forward_slash() {
2✔
422
        assert!(Function::parse("file(\"sub\\dir/\")").is_err());
2✔
423
    }
2✔
424

425
    #[test]
426
    fn function_parse_should_parse_a_file_size_function() {
2✔
427
        let output = Function::parse("file_size(\"Cargo.toml\", 1234)").unwrap();
2✔
428

429
        assert!(output.0.is_empty());
2✔
430
        match output.1 {
2✔
431
            Function::FileSize(f, s) => {
2✔
432
                assert_eq!(Path::new("Cargo.toml"), f);
2✔
433
                assert_eq!(1234, s);
2✔
434
            }
UNCOV
435
            _ => panic!("Expected a file size function"),
×
436
        }
437
    }
2✔
438

439
    #[test]
440
    fn function_parse_should_parse_a_readable_function() {
2✔
441
        let output = Function::parse("readable(\"Cargo.toml\")").unwrap();
2✔
442

443
        assert!(output.0.is_empty());
2✔
444
        match output.1 {
2✔
445
            Function::Readable(f) => assert_eq!(Path::new("Cargo.toml"), f),
2✔
UNCOV
446
            _ => panic!("Expected a readable function"),
×
447
        }
448
    }
2✔
449

450
    #[test]
451
    fn function_parse_should_parse_an_is_executable_function() {
2✔
452
        let output = Function::parse("is_executable(\"Cargo.toml\")").unwrap();
2✔
453

454
        assert!(output.0.is_empty());
2✔
455
        match output.1 {
2✔
456
            Function::IsExecutable(f) => assert_eq!(Path::new("Cargo.toml"), f),
2✔
UNCOV
457
            _ => panic!("Expected an is_executable function"),
×
458
        }
459
    }
2✔
460

461
    #[test]
462
    fn function_parse_should_parse_an_active_path_function() {
2✔
463
        let output = Function::parse("active(\"Cargo.toml\")").unwrap();
2✔
464

465
        assert!(output.0.is_empty());
2✔
466
        match output.1 {
2✔
467
            Function::ActivePath(f) => assert_eq!(Path::new("Cargo.toml"), f),
2✔
UNCOV
468
            _ => panic!("Expected an active path function"),
×
469
        }
470
    }
2✔
471

472
    #[test]
473
    fn function_parse_should_parse_an_active_regex_function() {
2✔
474
        let output = Function::parse("active(\"Cargo.*\")").unwrap();
2✔
475

476
        assert!(output.0.is_empty());
2✔
477
        match output.1 {
2✔
478
            Function::ActiveRegex(r) => {
2✔
479
                assert_eq!(Regex::new("^Cargo.*$").unwrap().as_str(), r.as_str());
2✔
480
            }
UNCOV
481
            _ => panic!("Expected an active regex function"),
×
482
        }
483
    }
2✔
484

485
    #[test]
486
    fn function_parse_should_parse_an_is_master_function() {
2✔
487
        let output = Function::parse("is_master(\"Blank.esm\")").unwrap();
2✔
488

489
        assert!(output.0.is_empty());
2✔
490
        match output.1 {
2✔
491
            Function::IsMaster(f) => assert_eq!(Path::new("Blank.esm"), f),
2✔
492
            _ => panic!("Expected an is master function"),
×
493
        }
494
    }
2✔
495

496
    #[test]
497
    fn function_parse_should_parse_a_many_function_with_no_parent_path() {
2✔
498
        let output = Function::parse("many(\"Cargo.*\")").unwrap();
2✔
499

500
        assert!(output.0.is_empty());
2✔
501
        match output.1 {
2✔
502
            Function::Many(p, r) => {
2✔
503
                assert_eq!(PathBuf::from("."), p);
2✔
504
                assert_eq!(Regex::new("^Cargo.*$").unwrap().as_str(), r.as_str());
2✔
505
            }
UNCOV
506
            _ => panic!("Expected a many function"),
×
507
        }
508
    }
2✔
509

510
    #[test]
511
    fn function_parse_should_parse_a_many_function_with_a_parent_path() {
2✔
512
        let output = Function::parse("many(\"subdir/Cargo.*\")").unwrap();
2✔
513

514
        assert!(output.0.is_empty());
2✔
515
        match output.1 {
2✔
516
            Function::Many(p, r) => {
2✔
517
                assert_eq!(PathBuf::from("subdir"), p);
2✔
518
                assert_eq!(Regex::new("^Cargo.*$").unwrap().as_str(), r.as_str());
2✔
519
            }
UNCOV
520
            _ => panic!("Expected a many function"),
×
521
        }
522
    }
2✔
523

524
    #[test]
525
    fn function_parse_should_error_if_given_a_many_function_ending_in_a_forward_slash() {
2✔
526
        assert!(Function::parse("many(\"subdir/\")").is_err());
2✔
527
    }
2✔
528

529
    #[test]
530
    fn function_parse_should_parse_a_many_active_function() {
2✔
531
        let output = Function::parse("many_active(\"Cargo.*\")").unwrap();
2✔
532

533
        assert!(output.0.is_empty());
2✔
534
        match output.1 {
2✔
535
            Function::ManyActive(r) => {
2✔
536
                assert_eq!(Regex::new("^Cargo.*$").unwrap().as_str(), r.as_str());
2✔
537
            }
UNCOV
538
            _ => panic!("Expected a many active function"),
×
539
        }
540
    }
2✔
541

542
    #[test]
543
    fn function_parse_should_parse_a_checksum_function() {
2✔
544
        let output = Function::parse("checksum(\"Cargo.toml\", DEADBEEF)").unwrap();
2✔
545

546
        assert!(output.0.is_empty());
2✔
547
        match output.1 {
2✔
548
            Function::Checksum(path, crc) => {
2✔
549
                assert_eq!(Path::new("Cargo.toml"), path);
2✔
550
                assert_eq!(0xDEAD_BEEF, crc);
2✔
551
            }
UNCOV
552
            _ => panic!("Expected a checksum function"),
×
553
        }
554
    }
2✔
555

556
    #[test]
557
    fn function_parse_should_parse_a_version_equals_function() {
2✔
558
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", ==)").unwrap();
2✔
559

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

571
    #[test]
572
    fn function_parse_should_parse_a_version_not_equals_function() {
2✔
573
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", !=)").unwrap();
2✔
574

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

586
    #[test]
587
    fn function_parse_should_parse_a_version_less_than_function() {
2✔
588
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", <)").unwrap();
2✔
589

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

601
    #[test]
602
    fn function_parse_should_parse_a_version_greater_than_function() {
2✔
603
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", >)").unwrap();
2✔
604

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

616
    #[test]
617
    fn function_parse_should_parse_a_version_less_than_or_equal_to_function() {
2✔
618
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", <=)").unwrap();
2✔
619

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

631
    #[test]
632
    fn function_parse_should_parse_a_version_greater_than_or_equal_to_function() {
2✔
633
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", >=)").unwrap();
2✔
634

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

646
    #[test]
647
    fn function_parse_should_parse_a_version_with_a_path_containing_backslashes() {
2✔
648
        let output = Function::parse("version(\"..\\Cargo.toml\", \"1.2\", ==)").unwrap();
2✔
649

650
        assert!(output.0.is_empty());
2✔
651
        match output.1 {
2✔
652
            Function::Version(path, version, comparator) => {
2✔
653
                assert_eq!(Path::new("..\\Cargo.toml"), path);
2✔
654
                assert_eq!("1.2", version);
2✔
655
                assert_eq!(ComparisonOperator::Equal, comparator);
2✔
656
            }
UNCOV
657
            _ => panic!("Expected a version function"),
×
658
        }
659
    }
2✔
660

661
    #[test]
662
    fn function_parse_should_parse_version_with_comparator_as_the_second_param() {
2✔
663
        let output = Function::parse("version(\"..\\Cargo.toml\", ==, \"1.2\")").unwrap();
2✔
664

665
        assert!(output.0.is_empty());
2✔
666
        match output.1 {
2✔
667
            Function::Version(path, version, comparator) => {
2✔
668
                assert_eq!(Path::new("..\\Cargo.toml"), path);
2✔
669
                assert_eq!("1.2", version);
2✔
670
                assert_eq!(ComparisonOperator::Equal, comparator);
2✔
671
            }
UNCOV
672
            _ => panic!("Expected a version function"),
×
673
        }
674
    }
2✔
675

676
    #[test]
677
    fn function_parse_should_parse_a_product_version_equals_function() {
2✔
678
        let output = Function::parse("product_version(\"Cargo.toml\", \"1.2\", ==)").unwrap();
2✔
679

680
        assert!(output.0.is_empty());
2✔
681
        match output.1 {
2✔
682
            Function::ProductVersion(path, version, comparator) => {
2✔
683
                assert_eq!(Path::new("Cargo.toml"), path);
2✔
684
                assert_eq!("1.2", version);
2✔
685
                assert_eq!(ComparisonOperator::Equal, comparator);
2✔
686
            }
UNCOV
687
            _ => panic!("Expected a product version function"),
×
688
        }
689
    }
2✔
690

691
    #[test]
692
    fn function_parse_should_parse_a_product_version_with_comparator_as_the_second_param() {
2✔
693
        let output = Function::parse("product_version(\"Cargo.toml\", ==, \"1.2\")").unwrap();
2✔
694

695
        assert!(output.0.is_empty());
2✔
696
        match output.1 {
2✔
697
            Function::ProductVersion(path, version, comparator) => {
2✔
698
                assert_eq!(Path::new("Cargo.toml"), path);
2✔
699
                assert_eq!("1.2", version);
2✔
700
                assert_eq!(ComparisonOperator::Equal, comparator);
2✔
701
            }
UNCOV
702
            _ => panic!("Expected a product version function"),
×
703
        }
704
    }
2✔
705

706
    #[test]
707
    fn function_parse_should_parse_a_filename_version_equals_function() {
2✔
708
        let output =
2✔
709
            Function::parse("filename_version(\"subdir/Cargo (.+).toml\", \"1.2\", ==)").unwrap();
2✔
710

711
        assert!(output.0.is_empty());
2✔
712
        match output.1 {
2✔
713
            Function::FilenameVersion(path, regex, version, comparator) => {
2✔
714
                assert_eq!(PathBuf::from("subdir"), path);
2✔
715
                assert_eq!(
2✔
716
                    Regex::new("^Cargo (.+).toml$").unwrap().as_str(),
2✔
717
                    regex.as_str()
2✔
718
                );
719
                assert_eq!("1.2", version);
2✔
720
                assert_eq!(ComparisonOperator::Equal, comparator);
2✔
721
            }
UNCOV
722
            _ => panic!("Expected a filename version function"),
×
723
        }
724
    }
2✔
725

726
    #[test]
727
    fn function_parse_should_parse_a_filename_version_with_comparator_as_the_second_param() {
2✔
728
        let output =
2✔
729
            Function::parse("filename_version(\"subdir/Cargo (.+).toml\", ==, \"1.2\")").unwrap();
2✔
730

731
        assert!(output.0.is_empty());
2✔
732
        match output.1 {
2✔
733
            Function::FilenameVersion(path, regex, version, comparator) => {
2✔
734
                assert_eq!(PathBuf::from("subdir"), path);
2✔
735
                assert_eq!(
2✔
736
                    Regex::new("^Cargo (.+).toml$").unwrap().as_str(),
2✔
737
                    regex.as_str()
2✔
738
                );
739
                assert_eq!("1.2", version);
2✔
740
                assert_eq!(ComparisonOperator::Equal, comparator);
2✔
741
            }
UNCOV
742
            _ => panic!("Expected a filename version function"),
×
743
        }
744
    }
2✔
745

746
    #[test]
747
    fn function_parse_should_error_if_the_filename_version_regex_does_not_contain_an_explicit_capture_group(
2✔
748
    ) {
2✔
749
        assert!(
2✔
750
            Function::parse("filename_version(\"subdir/Cargo .+.toml\", \"1.2\", ==)").is_err()
2✔
751
        );
752
    }
2✔
753

754
    #[test]
755
    fn function_parse_should_parse_a_description_contains_function() {
2✔
756
        let lowercase_non_ascii = "\u{20ac}\u{192}.";
2✔
757
        let function = format!("description_contains(\"Blank.esp\", \"{lowercase_non_ascii}\")");
2✔
758
        let output = Function::parse(&function).unwrap();
2✔
759

760
        assert!(output.0.is_empty());
2✔
761
        match output.1 {
2✔
762
            Function::DescriptionContains(p, r) => {
2✔
763
                assert_eq!(PathBuf::from("Blank.esp"), p);
2✔
764
                assert_eq!(
2✔
765
                    Regex::new(lowercase_non_ascii).unwrap().as_str(),
2✔
766
                    r.as_str()
2✔
767
                );
768
            }
UNCOV
769
            _ => panic!("Expected a description_contains function"),
×
770
        }
771
    }
2✔
772
}
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