OXYGENHUB

Coverage / crates/oxy-core/src/revset.rs 93.4%

394 of 422 lines run, 35 of 73 functions · run #298 on main · run missed

1
//! Revsets: expressions that select a set of changes (docs/revset_query_language.md).
2
//!
3
//! This module parses the language; [`crate::repo::Repo::revset`] evaluates
4
//! it over a repository. Grammar, from loosest to tightest:
5
//!
6
//! ```text
7
//! union      = inter ('|' inter)*
8
//! inter      = diff ('&' diff)*
9
//! diff       = range ('~' range)*            -- set difference, as in Jujutsu
10
//! range      = '::' primary | primary '::' primary? | primary '..' primary? | '..' primary | primary
11
//! primary    = '(' union ')' | name '(' args ')' | symbol | string
12
//! ```
13
//!
14
//! A symbol is a bookmark, `REMOTE/NAME`, a tag, a ChangeId prefix, a
15
//! `#hash` prefix or `@`; a quoted string is a function argument.
16
17
use std::fmt;
18
19
#[derive(Clone, Debug, PartialEq, Eq)]
20
pub enum Expr {
21
    /// A bookmark, tag, change prefix or `@`: the heads it names.
22
    Symbol(String),
23
    /// `::X`: X and its dependency closure.
24
    Ancestors(Box<Expr>),
25
    /// `X::`: X and everything depending on it.
26
    Descendants(Box<Expr>),
27
    /// `X::Y`: in both `X::` and `::Y`.
28
    Between(Box<Expr>, Box<Expr>),
29
    /// `X..Y`: `::Y ~ ::X`.
30
    Range(Box<Expr>, Box<Expr>),
31
    Union(Box<Expr>, Box<Expr>),
32
    Intersection(Box<Expr>, Box<Expr>),
33
    Difference(Box<Expr>, Box<Expr>),
34
    /// `name(args)`; a string argument is an `Expr::Symbol` holding its text.
35
    Call(String, Vec<Expr>),
36
}
37
38
impl fmt::Display for Expr {
3968×
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4068×
        match self {
4135×
            Expr::Symbol(s) => {
42125×
                if s.is_empty() || s.chars().any(|c| !is_symbol_char(c)) {
432×
                    write!(f, "{s:?}")
44
                } else {
4533×
                    f.write_str(s)
46
                }
47
            }
483×
            Expr::Ancestors(x) => write!(f, "::{x}"),
491×
            Expr::Descendants(x) => write!(f, "{x}::"),
501×
            Expr::Between(x, y) => write!(f, "{x}::{y}"),
514×
            Expr::Range(x, y) => write!(f, "{x}..{y}"),
523×
            Expr::Union(x, y) => write!(f, "({x} | {y})"),
535×
            Expr::Intersection(x, y) => write!(f, "({x} & {y})"),
545×
            Expr::Difference(x, y) => write!(f, "({x} ~ {y})"),
5511×
            Expr::Call(name, args) => {
5611×
                write!(f, "{name}(")?;
5711×
                for (i, a) in args.iter().enumerate() {
588×
                    if i > 0 {
591×
                        f.write_str(", ")?;
607×
                    }
618×
                    write!(f, "{a}")?;
62
                }
6311×
                f.write_str(")")
64
            }
65
        }
6668×
    }
67
}
68
69
#[derive(Clone, Debug, PartialEq, Eq)]
70
pub struct ParseError {
71
    pub at: usize,
72
    pub message: String,
73
}
74
75
impl fmt::Display for ParseError {
762×
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
772×
        write!(f, "{} (at character {})", self.message, self.at + 1)
782×
    }
79
}
80
81
impl std::error::Error for ParseError {}
82
83
#[derive(Clone, Debug, PartialEq, Eq)]
84
enum Tok {
85
    Symbol(String),
86
    Str(String),
87
    ColonColon,
88
    DotDot,
89
    Tilde,
90
    Amp,
91
    Pipe,
92
    LParen,
93
    RParen,
94
    Comma,
95
}
96
971466×
fn is_symbol_char(c: char) -> bool {
981466×
    c.is_alphanumeric() || matches!(c, '_' | '.' | '/' | '@' | '#' | '-' | '+')
991466×
}
100
101100×
fn lex(src: &str) -> Result<Vec<(usize, Tok)>, ParseError> {
102100×
    let mut out = Vec::new();
103100×
    let chars: Vec<(usize, char)> = src.char_indices().collect();
104100×
    let mut i = 0;
105592×
    while i < chars.len() {
106494×
        let (at, c) = chars[i];
107182×
        match c {
108494×
            c if c.is_whitespace() => i += 1,
109
            ':' => {
11032×
                if chars.get(i + 1).map(|x| x.1) == Some(':') {
11131×
                    out.push((at, Tok::ColonColon));
11231×
                    i += 2;
11331×
                } else {
1141×
                    return Err(ParseError {
1151×
                        at,
1161×
                        message: "expected `::`".into(),
1171×
                    });
118
                }
119
            }
12012×
            '.' if chars.get(i + 1).map(|x| x.1) == Some('.') => {
12112×
                out.push((at, Tok::DotDot));
12212×
                i += 2;
12312×
            }
1249×
            '~' => {
1259×
                out.push((at, Tok::Tilde));
1269×
                i += 1;
1279×
            }
12823×
            '&' => {
12923×
                out.push((at, Tok::Amp));
13023×
                i += 1;
13123×
            }
1326×
            '|' => {
1336×
                out.push((at, Tok::Pipe));
1346×
                i += 1;
1356×
            }
13667×
            '(' => {
13767×
                out.push((at, Tok::LParen));
13867×
                i += 1;
13967×
            }
14067×
            ')' => {
14167×
                out.push((at, Tok::RParen));
14267×
                i += 1;
14367×
            }
1445×
            ',' => {
1455×
                out.push((at, Tok::Comma));
1465×
                i += 1;
1475×
            }
148
            '"' | '\'' => {
14911×
                let quote = c;
15011×
                let mut s = String::new();
15111×
                let mut j = i + 1;
152
                loop {
15394×
                    match chars.get(j) {
154
                        None => {
1551×
                            return Err(ParseError {
1561×
                                at,
1571×
                                message: "unterminated string".into(),
1581×
                            })
159
                        }
16093×
                        Some((_, ch)) if *ch == quote => break,
161
                        Some((_, '\\')) => {
1620×
                            if let Some((_, next)) = chars.get(j + 1) {
1630×
                                s.push(*next);
1640×
                                j += 2;
1650×
                            } else {
1660×
                                j += 1;
1670×
                            }
168
                        }
16983×
                        Some((_, ch)) => {
17083×
                            s.push(*ch);
17183×
                            j += 1;
17283×
                        }
173
                    }
174
                }
17510×
                out.push((at, Tok::Str(s)));
17610×
                i = j + 1;
177
            }
178182×
            c if is_symbol_char(c) => {
179182×
                let mut s = String::new();
180182×
                let mut j = i;
181
                // `..` ends a symbol: `main..feature`.
1821200×
                while let Some((_, ch)) = chars.get(j) {
1831159×
                    if !is_symbol_char(*ch) {
184132×
                        break;
1851027×
                    }
1861027×
                    if *ch == '.' && chars.get(j + 1).map(|x| x.1) == Some('.') {
1879×
                        break;
1881018×
                    }
1891018×
                    s.push(*ch);
1901018×
                    j += 1;
191
                }
192182×
                out.push((at, Tok::Symbol(s)));
193182×
                i = j;
194
            }
1950×
            other => {
1960×
                return Err(ParseError {
1970×
                    at,
1980×
                    message: format!("unexpected character {other:?}"),
1990×
                })
200
            }
201
        }
202
    }
20398×
    Ok(out)
204100×
}
205
206
struct Parser {
207
    src: String,
208
    toks: Vec<(usize, Tok)>,
209
    pos: usize,
210
    end: usize,
211
}
212
213
/// Functions whose argument is taken as written, not parsed: a date range
214
/// such as `-7d..` would otherwise read as a revset range.
215
const RAW_ARGS: [&str; 1] = ["date"];
216
217
impl Parser {
2181681×
    fn peek(&self) -> Option<&Tok> {
2191681×
        self.toks.get(self.pos).map(|t| &t.1)
2201681×
    }
22114×
    fn at(&self) -> usize {
22214×
        self.toks.get(self.pos).map(|t| t.0).unwrap_or(self.end)
22314×
    }
2241463×
    fn eat(&mut self, t: &Tok) -> bool {
2251463×
        if self.peek() == Some(t) {
226209×
            self.pos += 1;
227209×
            true
228
        } else {
2291254×
            false
230
        }
2311463×
    }
2326×
    fn err<T>(&self, message: impl Into<String>) -> Result<T, ParseError> {
2336×
        Err(ParseError {
2346×
            at: self.at(),
2356×
            message: message.into(),
2366×
        })
2376×
    }
238
239147×
    fn union(&mut self) -> Result<Expr, ParseError> {
240147×
        let mut left = self.inter()?;
241147×
        while self.eat(&Tok::Pipe) {
2426×
            let right = self.inter()?;
2436×
            left = Expr::Union(Box::new(left), Box::new(right));
244
        }
245141×
        Ok(left)
246147×
    }
247
248153×
    fn inter(&mut self) -> Result<Expr, ParseError> {
249153×
        let mut left = self.diff()?;
250170×
        while self.eat(&Tok::Amp) {
25123×
            let right = self.diff()?;
25219×
            left = Expr::Intersection(Box::new(left), Box::new(right));
253
        }
254147×
        Ok(left)
255153×
    }
256
257176×
    fn diff(&mut self) -> Result<Expr, ParseError> {
258176×
        let mut left = self.range()?;
259179×
        while self.eat(&Tok::Tilde) {
2609×
            let right = self.range()?;
2619×
            left = Expr::Difference(Box::new(left), Box::new(right));
262
        }
263170×
        Ok(left)
264176×
    }
265
266
    /// Whether the next token starts a primary expression.
26710×
    fn starts_primary(&self) -> bool {
2685×
        matches!(
26910×
            self.peek(),
270
            Some(Tok::Symbol(_) | Tok::Str(_) | Tok::LParen)
271
        )
27210×
    }
273
274185×
    fn range(&mut self) -> Result<Expr, ParseError> {
275185×
        if self.eat(&Tok::ColonColon) {
27627×
            let x = self.primary()?;
27727×
            return Ok(Expr::Ancestors(Box::new(x)));
278158×
        }
279158×
        if self.eat(&Tok::DotDot) {
2802×
            let y = self.primary()?;
2812×
            return Ok(Expr::Ancestors(Box::new(y)));
282156×
        }
283156×
        let x = self.primary()?;
284150×
        if self.eat(&Tok::ColonColon) {
2854×
            if self.starts_primary() {
2862×
                let y = self.primary()?;
2872×
                return Ok(Expr::Between(Box::new(x), Box::new(y)));
2882×
            }
2892×
            return Ok(Expr::Descendants(Box::new(x)));
290146×
        }
291146×
        if self.eat(&Tok::DotDot) {
2926×
            if self.starts_primary() {
2933×
                let y = self.primary()?;
2943×
                return Ok(Expr::Range(Box::new(x), Box::new(y)));
2953×
            }
296
            // `X..`: what the repository has beyond X.
2973×
            return Ok(Expr::Range(
2983×
                Box::new(x),
2993×
                Box::new(Expr::Call("all".into(), vec![])),
3003×
            ));
301140×
        }
302140×
        Ok(x)
303185×
    }
304
305190×
    fn primary(&mut self) -> Result<Expr, ParseError> {
306190×
        match self.peek().cloned() {
307
            Some(Tok::LParen) => {
3081×
                self.pos += 1;
3091×
                let e = self.union()?;
3101×
                if !self.eat(&Tok::RParen) {
3110×
                    return self.err("expected `)`");
3121×
                }
3131×
                Ok(e)
314
            }
3159×
            Some(Tok::Str(s)) => {
3169×
                self.pos += 1;
3179×
                Ok(Expr::Symbol(s))
318
            }
319175×
            Some(Tok::Symbol(s)) => {
320175×
                self.pos += 1;
321175×
                if self.eat(&Tok::LParen) {
32266×
                    if RAW_ARGS.contains(&s.as_str()) {
3235×
                        return self.raw_call(s);
32461×
                    }
32561×
                    let mut args = Vec::new();
32661×
                    if !self.eat(&Tok::RParen) {
327
                        loop {
32849×
                            args.push(self.union()?);
32948×
                            if self.eat(&Tok::Comma) {
3305×
                                continue;
33143×
                            }
33243×
                            if self.eat(&Tok::RParen) {
33343×
                                break;
3340×
                            }
3350×
                            return self.err("expected `,` or `)` in the arguments");
336
                        }
33717×
                    }
33860×
                    return Ok(Expr::Call(s, args));
339109×
                }
340109×
                Ok(Expr::Symbol(s))
341
            }
3421×
            Some(Tok::RParen) => self.err("unexpected `)`"),
3430×
            Some(_) => self.err("expected a change, bookmark, tag or function here"),
3444×
            None => self.err("the expression ends too soon"),
345
        }
346190×
    }
347
}
348
349
impl Parser {
350
    /// `name(` has been consumed: the text up to the matching `)` is the
351
    /// one argument, or a quoted string is.
3525×
    fn raw_call(&mut self, name: String) -> Result<Expr, ParseError> {
3531×
        if let (Some(Tok::Str(s)), Some(Tok::RParen)) = (
3545×
            self.peek().cloned(),
3555×
            self.toks.get(self.pos + 1).map(|t| &t.1),
356
        ) {
3571×
            self.pos += 2;
3581×
            return Ok(Expr::Call(name, vec![Expr::Symbol(s)]));
3594×
        }
3604×
        let start = self.at();
3614×
        let mut depth = 0usize;
36213×
        while let Some(t) = self.peek() {
3634×
            match t {
3640×
                Tok::LParen => depth += 1,
3654×
                Tok::RParen if depth == 0 => {
3664×
                    let end = self.at();
3674×
                    self.pos += 1;
3684×
                    let raw = self.src[start..end].trim().to_string();
3694×
                    return Ok(Expr::Call(name, vec![Expr::Symbol(raw)]));
370
                }
3710×
                Tok::RParen => depth -= 1,
3729×
                _ => {}
373
            }
3749×
            self.pos += 1;
375
        }
3760×
        self.err(format!("expected `)` to end {name}(…)"))
3775×
    }
378
}
379
380
/// Parse a revset expression.
381100×
pub fn parse(src: &str) -> Result<Expr, ParseError> {
382100×
    let toks = lex(src)?;
38398×
    if toks.is_empty() {
3841×
        return Err(ParseError {
3851×
            at: 0,
3861×
            message: "empty revset".into(),
3871×
        });
38897×
    }
38997×
    let mut p = Parser {
39097×
        src: src.to_string(),
39197×
        toks,
39297×
        pos: 0,
39397×
        end: src.len(),
39497×
    };
39597×
    let e = p.union()?;
39692×
    if p.pos < p.toks.len() {
3971×
        return p.err("unexpected input after the expression");
39891×
    }
39991×
    Ok(e)
400100×
}
401
402
/// A date range for `date(RANGE)`: wall-clock milliseconds, inclusive at
403
/// both ends, either end open.
404
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
405
pub struct DateRange {
406
    pub from: Option<u64>,
407
    pub to: Option<u64>,
408
}
409
410
impl DateRange {
41110×
    pub fn contains(&self, ms: u64) -> bool {
41210×
        self.from.is_none_or(|f| ms >= f) && self.to.is_none_or(|t| ms <= t)
41310×
    }
414
}
415
416
/// `2026-10-01..`, `..2026-09-30`, `2026-09-01..2026-09-30`, `-7d..`,
417
/// `-12h..-1h`, or a single point, which means that day (or, for a
418
/// relative time, from then on). Dates are UTC; a time of day may follow
419
/// as `T13:45` or `T13:45:00`, with an optional `Z`.
42010×
pub fn parse_date_range(s: &str, now_ms: u64) -> Result<DateRange, String> {
42110×
    let s = s.trim();
42210×
    let (a, b, dotted) = match s.find("..") {
4236×
        Some(i) => (&s[..i], &s[i + 2..], true),
4244×
        None => (s, "", false),
425
    };
42610×
    let from = if a.is_empty() {
4272×
        None
428
    } else {
4298×
        Some(parse_point(a, now_ms)?.0)
430
    };
4318×
    let to = if dotted {
4326×
        if b.is_empty() {
4332×
            None
434
        } else {
4354×
            Some(parse_point(b, now_ms)?.1)
436
        }
4372×
    } else if a.starts_with('-') {
4380×
        None
439
    } else {
4402×
        Some(parse_point(a, now_ms)?.1)
441
    };
4428×
    if let (Some(f), Some(t)) = (from, to) {
4434×
        if f > t {
4441×
            return Err(format!("date range {s:?} ends before it starts"));
4453×
        }
4464×
    }
4477×
    Ok(DateRange { from, to })
44810×
}
449
450
/// A point in time as the (start, end) of what it names: a day spans its
451
/// 24 hours, a minute its 60 seconds, a relative time is an instant.
45214×
fn parse_point(s: &str, now_ms: u64) -> Result<(u64, u64), String> {
45314×
    let s = s.trim();
45414×
    if let Some(rel) = s.strip_prefix('-') {
4554×
        let (n, unit) = rel.split_at(rel.find(|c: char| !c.is_ascii_digit()).unwrap_or(rel.len()));
4562×
        let n: u64 = n
4572×
            .parse()
4582×
            .map_err(|_| format!("{s:?}: a relative time is -7d, -12h or -30m"))?;
4592×
        let ms = match unit {
4602×
            "d" => n * 86_400_000,
4611×
            "h" => n * 3_600_000,
4620×
            "m" => n * 60_000,
4630×
            "w" => n * 7 * 86_400_000,
4640×
            _ => return Err(format!("{s:?}: a relative time is -7d, -12h or -30m")),
465
        };
4662×
        let at = now_ms.saturating_sub(ms);
4672×
        return Ok((at, at));
46812×
    }
46912×
    let (date, time) = match s.split_once('T') {
4702×
        Some((d, t)) => (d, Some(t.trim_end_matches('Z'))),
47110×
        None => (s, None),
472
    };
47312×
    let mut parts = date.split('-');
47412×
    let bad = || format!("{s:?}: a date is YYYY-MM-DD, with an optional THH:MM[:SS]");
47512×
    let y: i64 = parts.next().and_then(|p| p.parse().ok()).ok_or_else(bad)?;
47611×
    let m: i64 = parts.next().and_then(|p| p.parse().ok()).ok_or_else(bad)?;
47711×
    let d: i64 = parts.next().and_then(|p| p.parse().ok()).ok_or_else(bad)?;
47811×
    if parts.next().is_some() || !(1..=12).contains(&m) || !(1..=31).contains(&d) {
4791×
        return Err(bad());
48010×
    }
48110×
    let day_ms = days_from_civil(y, m, d) * 86_400_000;
48210×
    if day_ms < 0 {
4830×
        return Err(bad());
48410×
    }
48510×
    let day_ms = day_ms as u64;
48610×
    match time {
4878×
        None => Ok((day_ms, day_ms + 86_400_000 - 1)),
4882×
        Some(t) => {
4892×
            let mut hms = t.split(':');
4902×
            let h: u64 = hms.next().and_then(|p| p.parse().ok()).ok_or_else(bad)?;
4912×
            let mi: u64 = hms.next().and_then(|p| p.parse().ok()).ok_or_else(bad)?;
4922×
            let sec: Option<u64> = match hms.next() {
4930×
                Some(p) => Some(p.parse().map_err(|_| bad())?),
4942×
                None => None,
495
            };
4962×
            if hms.next().is_some() || h > 23 || mi > 59 || sec.is_some_and(|x| x > 59) {
4970×
                return Err(bad());
4982×
            }
4992×
            let start = day_ms + h * 3_600_000 + mi * 60_000 + sec.unwrap_or(0) * 1000;
5002×
            let span = if sec.is_some() { 1000 } else { 60_000 };
5012×
            Ok((start, start + span - 1))
502
        }
503
    }
50414×
}
505
506
/// Days since 1970-01-01 of a civil date (Howard Hinnant's algorithm).
50715×
fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
50815×
    let y = if m <= 2 { y - 1 } else { y };
50915×
    let era = y.div_euclid(400);
51015×
    let yoe = y - era * 400;
51115×
    let mp = (m + 9) % 12;
51215×
    let doy = (153 * mp + 2) / 5 + d - 1;
51315×
    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
51415×
    era * 146_097 + doe - 719_468
51515×
}
516
517
#[cfg(test)]
518
mod tests {
519
    use super::*;
520
52120×
    fn p(s: &str) -> String {
52220×
        parse(s).unwrap().to_string()
52320×
    }
524
525
    #[test]
5261×
    fn precedence_and_ranges() {
5271×
        assert_eq!(p("a | b & c ~ d"), "(a | (b & (c ~ d)))");
5281×
        assert_eq!(p("a ~ b & c"), "((a ~ b) & c)");
5291×
        assert_eq!(p("(a | b) & c"), "((a | b) & c)");
5301×
        assert_eq!(p("::main"), "::main");
5311×
        assert_eq!(p("main::"), "main::");
5321×
        assert_eq!(p("a::b"), "a::b");
5331×
        assert_eq!(p("main..feature"), "main..feature");
5341×
        assert_eq!(p("..feature"), "::feature");
5351×
        assert_eq!(p("main.."), "main..all()");
5361×
        assert_eq!(p("origin/main..@"), "origin/main..@");
5371×
        assert_eq!(p("#fc68459c & v0.3.1"), "(#fc68459c & v0.3.1)");
5381×
        assert_eq!(p("a ~ b ~ c"), "((a ~ b) ~ c)");
5391×
    }
540
541
    #[test]
5421×
    fn calls_and_strings() {
5431×
        assert_eq!(
5441×
            p("touches(\"src/**\") & date(-7d..)"),
545
            "(touches(\"src/**\") & date(-7d..))"
546
        );
5471×
        assert_eq!(p("author('Andy McCall')"), "author(\"Andy McCall\")");
5481×
        assert_eq!(p("latest(main.., 5)"), "latest(main..all(), 5)");
5491×
        assert_eq!(
5501×
            p("date(2026-09-01..2026-09-30)"),
551
            "date(2026-09-01..2026-09-30)"
552
        );
5531×
        assert_eq!(p("date(\"..-1h\")"), "date(..-1h)");
5541×
        assert_eq!(p("all() ~ ::main"), "(all() ~ ::main)");
5551×
        assert_eq!(p("heads(a | b)"), "heads((a | b))");
5561×
        assert_eq!(p("none()"), "none()");
5571×
    }
558
559
    #[test]
5601×
    fn errors_point_at_the_problem() {
5611×
        assert!(parse("").is_err());
5621×
        assert_eq!(parse("a &").unwrap_err().at, 3);
5631×
        assert!(parse("a & )").is_err());
5641×
        assert!(parse("f(a, ").is_err());
5651×
        assert!(parse("'open").is_err());
5661×
        assert!(parse("a : b").is_err());
5671×
        assert!(parse("a b").is_err());
5681×
    }
569
570
    #[test]
5711×
    fn dates() {
5721×
        let now = 1_790_000_000_000; // 2026-09-21T06:13:20Z
5731×
        let r = parse_date_range("2026-09-01..2026-09-30", now).unwrap();
5741×
        assert_eq!(
575
            r.from,
5761×
            Some(days_from_civil(2026, 9, 1) as u64 * 86_400_000)
577
        );
5781×
        assert_eq!(
579
            r.to,
5801×
            Some(days_from_civil(2026, 9, 30) as u64 * 86_400_000 + 86_399_999)
581
        );
5821×
        assert!(r.contains(days_from_civil(2026, 9, 15) as u64 * 86_400_000 + 1));
5831×
        let r = parse_date_range("-7d..", now).unwrap();
5841×
        assert_eq!(
585
            r,
5861×
            DateRange {
5871×
                from: Some(now - 7 * 86_400_000),
5881×
                to: None
5891×
            }
590
        );
5911×
        let r = parse_date_range("..2026-01-01", now).unwrap();
5921×
        assert!(r.from.is_none() && r.to.is_some());
5931×
        let day = parse_date_range("2026-10-03", now).unwrap();
5941×
        assert_eq!(day.to.unwrap() - day.from.unwrap(), 86_399_999);
5951×
        let minute = parse_date_range("2026-10-03T09:30Z", now).unwrap();
5961×
        assert_eq!(minute.to.unwrap() - minute.from.unwrap(), 59_999);
5971×
        assert!(parse_date_range("2026-13-01", now).is_err());
5981×
        assert!(parse_date_range("yesterday", now).is_err());
5991×
        assert!(parse_date_range("2026-09-30..2026-09-01", now).is_err());
6001×
        assert_eq!(days_from_civil(1970, 1, 1), 0);
6011×
        assert_eq!(days_from_civil(2000, 3, 1), 11_017);
6021×
    }
603
}