1use std::path::Path;
10
11use crate::engine_contract::{EntryKind, Error, Result};
12use crate::query::query_glob::Pattern;
13
14#[derive(Clone, Copy, PartialEq, Eq, Debug)]
16pub enum SizeMetric {
17 Apparent,
19 Allocated,
21}
22
23impl SizeMetric {
24 pub const fn label(self) -> &'static str {
30 match self {
31 Self::Apparent => "apparent",
32 Self::Allocated => "allocated",
33 }
34 }
35}
36
37impl Default for SizeMetric {
43 fn default() -> Self {
44 crate::query::Request::DEFAULTS.size
45 }
46}
47
48#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
50pub enum SortKey {
51 #[default]
53 Size,
54 Count,
56 Mtime,
58 Name,
60}
61
62#[derive(Clone, Copy, Debug, Default)]
69pub struct ModifiedWindow {
70 pub since: Option<i64>,
72 pub before: Option<i64>,
74}
75
76impl ModifiedWindow {
77 pub fn contains(&self, mtime_ns: i64) -> bool {
79 self.since.is_none_or(|since| mtime_ns >= since)
80 && self.before.is_none_or(|before| mtime_ns < before)
81 }
82
83 pub fn is_unbounded(&self) -> bool {
85 self.since.is_none() && self.before.is_none()
86 }
87}
88
89#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
96pub enum IgnoredEntries {
97 #[default]
99 Include,
100 Exclude,
102 Only,
104}
105
106impl IgnoredEntries {
107 pub const fn admits(self, ignored: bool) -> bool {
109 match self {
110 Self::Include => true,
111 Self::Exclude => !ignored,
112 Self::Only => ignored,
113 }
114 }
115
116 pub fn parse(value: &str) -> std::result::Result<Self, String> {
121 match value.trim().to_ascii_lowercase().as_str() {
122 "include" => Ok(Self::Include),
123 "exclude" => Ok(Self::Exclude),
124 "only" => Ok(Self::Only),
125 _ => Err("expected one of include, exclude, only".to_string()),
126 }
127 }
128
129 pub const fn label(self) -> &'static str {
131 match self {
132 Self::Include => "include",
133 Self::Exclude => "exclude",
134 Self::Only => "only",
135 }
136 }
137}
138
139#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
144pub enum Bound {
145 #[default]
147 All,
148 Limit(usize),
150}
151
152impl Bound {
153 pub fn admits(self, index: usize) -> bool {
155 match self {
156 Self::All => true,
157 Self::Limit(limit) => index < limit,
158 }
159 }
160
161 pub fn limit(self) -> Option<usize> {
163 match self {
164 Self::All => None,
165 Self::Limit(limit) => Some(limit),
166 }
167 }
168}
169
170#[derive(Clone, Debug, Default)]
172pub struct Selection {
173 pub include: Vec<Pattern>,
175 pub exclude: Vec<Pattern>,
177 pub min_size: Option<u64>,
179 pub kinds: Vec<EntryKind>,
181 pub modified: ModifiedWindow,
183 pub ignored: IgnoredEntries,
188 pub depth: Option<Bound>,
196 pub limit: Option<Bound>,
203 pub sort: Option<SortKey>,
209 pub reverse: bool,
211 pub size: SizeMetric,
213}
214
215#[derive(Clone, Copy, Debug)]
226pub struct Candidate<'a> {
227 pub relative: &'a Path,
229 pub name: &'a str,
231 pub kind: EntryKind,
233 pub bytes: u64,
235 pub allocated: u64,
237 pub mtime_ns: i64,
239 pub ignored: bool,
244}
245
246#[derive(Clone, Debug, Default)]
269pub struct EntrySelection {
270 pub query: Selection,
272 pub max_size: Option<u64>,
276 pub exclude_ignored: bool,
278 pub logical_extensions: Vec<String>,
282 pub exact_names: Vec<String>,
287 pub terminal_extensions: Vec<String>,
293 pub ancestor_names: Vec<String>,
298}
299
300#[derive(Clone, Copy, PartialEq, Eq, Debug)]
302pub(crate) enum NameIdentity {
303 Native,
305 Portable,
307}
308
309impl From<Selection> for EntrySelection {
310 fn from(query: Selection) -> Self {
311 Self { query, ..Self::default() }
312 }
313}
314
315impl Selection {
316 pub(crate) fn retained_heap_bytes(&self) -> usize {
318 let pattern_bytes =
319 self.include.iter().chain(&self.exclude).fold(0_usize, |total, pattern| {
320 total.saturating_add(pattern.retained_heap_bytes())
321 });
322 self.include
323 .capacity()
324 .saturating_add(self.exclude.capacity())
325 .saturating_mul(std::mem::size_of::<Pattern>())
326 .saturating_add(pattern_bytes)
327 .saturating_add(self.kinds.capacity().saturating_mul(std::mem::size_of::<EntryKind>()))
328 }
329
330 pub fn is_unfiltered(&self) -> bool {
336 self.include.is_empty()
337 && self.exclude.is_empty()
338 && self.min_size.is_none()
339 && self.kinds.is_empty()
340 && self.modified.is_unbounded()
341 && self.ignored == IgnoredEntries::Include
342 }
343
344 pub fn admits(&self, candidate: &Candidate<'_>) -> bool {
346 if !self.kinds.is_empty() && !self.kinds.contains(&candidate.kind) {
347 return false;
348 }
349 if !self.ignored.admits(candidate.ignored) {
350 return false;
351 }
352 if let Some(min_size) = self.min_size {
353 if self.size_of(candidate) < min_size {
354 return false;
355 }
356 }
357 if !self.modified.contains(candidate.mtime_ns) {
358 return false;
359 }
360 if self.exclude.iter().any(|p| p.matches(candidate.relative, candidate.name)) {
363 return false;
364 }
365 if self.include.is_empty() {
366 return true;
367 }
368 self.include.iter().any(|p| p.matches(candidate.relative, candidate.name))
369 }
370
371 pub fn size_of(&self, candidate: &Candidate<'_>) -> u64 {
373 match self.size {
374 SizeMetric::Apparent => candidate.bytes,
375 SizeMetric::Allocated => candidate.allocated,
376 }
377 }
378}
379
380impl EntrySelection {
381 pub fn admit_terminal_extension(&mut self, value: impl Into<String>) -> Result<()> {
388 let value = value.into();
389 if self.terminal_extensions.contains(&value) {
390 return Err(refusal(TERMINAL_KIND, &value, TERMINAL_UNIQUE));
391 }
392 check_value(TERMINAL_KIND, &value, TERMINAL_RULES)?;
393 self.terminal_extensions.push(value);
394 Ok(())
395 }
396
397 pub fn admit_ancestor_name(&mut self, value: impl Into<String>) -> Result<()> {
404 let value = value.into();
405 if self.ancestor_names.contains(&value) {
406 return Err(refusal(ANCESTOR_KIND, &value, ANCESTOR_UNIQUE));
407 }
408 check_value(ANCESTOR_KIND, &value, ANCESTOR_RULES)?;
409 self.ancestor_names.push(value);
410 Ok(())
411 }
412
413 pub fn validate(&self) -> Result<()> {
425 check_list(TERMINAL_KIND, &self.terminal_extensions, TERMINAL_UNIQUE, TERMINAL_RULES)?;
426 check_list(ANCESTOR_KIND, &self.ancestor_names, ANCESTOR_UNIQUE, ANCESTOR_RULES)
427 }
428
429 pub(crate) fn retained_heap_bytes(&self) -> usize {
431 self.query
432 .retained_heap_bytes()
433 .saturating_add(retained_strings(
434 &self.logical_extensions,
435 self.logical_extensions.capacity(),
436 ))
437 .saturating_add(retained_strings(&self.exact_names, self.exact_names.capacity()))
438 .saturating_add(retained_strings(
439 &self.terminal_extensions,
440 self.terminal_extensions.capacity(),
441 ))
442 .saturating_add(retained_strings(&self.ancestor_names, self.ancestor_names.capacity()))
443 }
444
445 pub fn is_unfiltered(&self) -> bool {
447 self.query.is_unfiltered()
448 && self.max_size.is_none()
449 && !self.exclude_ignored
450 && self.logical_extensions.is_empty()
451 && self.exact_names.is_empty()
452 && self.terminal_extensions.is_empty()
453 && self.ancestor_names.is_empty()
454 }
455
456 pub fn admits(&self, candidate: &Candidate<'_>) -> bool {
458 if !self.query.admits(candidate) {
459 return false;
460 }
461 if let Some(max_size) = self.max_size {
462 if self.query.size_of(candidate) > max_size {
463 return false;
464 }
465 }
466 if self.exclude_ignored && candidate.ignored {
467 return false;
468 }
469 if !self.logical_extensions.is_empty() || !self.exact_names.is_empty() {
470 if candidate.kind != EntryKind::File {
471 return false;
472 }
473 let extension_matches = crate::classify::logical_ext(candidate.name.as_ref())
474 .is_some_and(|extension| {
475 self.logical_extensions
476 .iter()
477 .any(|expected| extension.eq_ignore_ascii_case(expected))
478 });
479 let name_matches = self
480 .exact_names
481 .iter()
482 .any(|expected| candidate.name.eq_ignore_ascii_case(expected));
483 if !extension_matches && !name_matches {
484 return false;
485 }
486 }
487 if !self.terminal_extensions.is_empty() {
488 if candidate.kind != EntryKind::File {
489 return false;
490 }
491 let Some(suffix) = terminal_suffix(candidate.name) else {
492 return false;
493 };
494 if !self
495 .terminal_extensions
496 .iter()
497 .any(|expected| suffix.eq_ignore_ascii_case(expected))
498 {
499 return false;
500 }
501 }
502 if !self.ancestor_names.is_empty()
503 && !candidate.relative.parent().is_some_and(|parent| {
504 parent.components().any(|component| {
505 let std::path::Component::Normal(name) = component else {
506 return false;
507 };
508 self.ancestor_names.iter().any(|expected| name == expected.as_str())
509 })
510 })
511 {
512 return false;
513 }
514 true
515 }
516}
517
518const TERMINAL_KIND: &str = "terminal extension";
519const ANCESTOR_KIND: &str = "ancestor name";
520const TERMINAL_UNIQUE: &str = "terminal_extensions entries must be unique";
521const ANCESTOR_UNIQUE: &str = "ancestor_names entries must be unique";
522
523type Rule = (fn(&str) -> bool, &'static str);
525
526const TERMINAL_RULES: &[Rule] = &[
531 (undotted, "terminal_extensions entries must start with a dot"),
532 (not_lowercase, "terminal_extensions entries must be lowercase"),
533 (not_terminal_suffix, "terminal_extensions entries must be canonical terminal suffixes"),
534];
535
536const ANCESTOR_RULES: &[Rule] =
538 &[(not_path_component, "ancestor_names entries must be exact path-component names")];
539
540fn undotted(value: &str) -> bool {
541 !value.starts_with('.')
542}
543
544fn not_lowercase(value: &str) -> bool {
547 value.to_lowercase() != value
548}
549
550fn not_terminal_suffix(value: &str) -> bool {
551 value.chars().count() < 2 || value.contains(['/', '\\']) || value[1..].contains('.')
552}
553
554fn not_path_component(value: &str) -> bool {
555 value.is_empty() || value == "." || value == ".." || value.contains(['/', '\\'])
556}
557
558fn refusal(kind: &'static str, value: &str, hint: &str) -> Error {
559 Error::InvalidValue { kind, value: value.to_owned(), hint: hint.to_owned() }
560}
561
562fn check_value(kind: &'static str, value: &str, rules: &[Rule]) -> Result<()> {
564 match rules.iter().find(|(breaks, _)| breaks(value)) {
565 Some((_, hint)) => Err(refusal(kind, value, hint)),
566 None => Ok(()),
567 }
568}
569
570fn check_list(kind: &'static str, values: &[String], unique: &str, rules: &[Rule]) -> Result<()> {
573 let mut seen = std::collections::HashSet::with_capacity(values.len());
574 if let Some(repeated) = values.iter().find(|value| !seen.insert(value.as_str())) {
575 return Err(refusal(kind, repeated, unique));
576 }
577 for (breaks, hint) in rules {
578 if let Some(value) = values.iter().find(|value| breaks(value)) {
579 return Err(refusal(kind, value, hint));
580 }
581 }
582 Ok(())
583}
584
585fn retained_strings(values: &[String], capacity: usize) -> usize {
586 capacity.saturating_mul(std::mem::size_of::<String>()).saturating_add(
587 values.iter().fold(0_usize, |total, value| total.saturating_add(value.capacity())),
588 )
589}
590
591fn terminal_suffix(name: &str) -> Option<&str> {
592 let dot = name.rfind('.')?;
593 (dot > 0 && dot + 1 < name.len()).then_some(&name[dot..])
594}
595
596#[cfg(test)]
597mod tests {
598 use super::*;
599 use std::path::PathBuf;
600
601 fn candidate(path: &str, kind: EntryKind, bytes: u64, mtime_ns: i64) -> (PathBuf, String) {
602 let relative = PathBuf::from(path);
603 let name = relative
604 .file_name()
605 .map(|name| name.to_string_lossy().into_owned())
606 .unwrap_or_default();
607 let _ = (kind, bytes, mtime_ns);
608 (relative, name)
609 }
610
611 fn entry_admits(
612 selection: &EntrySelection,
613 path: &str,
614 kind: EntryKind,
615 bytes: u64,
616 mtime: i64,
617 ignored: bool,
618 ) -> bool {
619 let (relative, name) = candidate(path, kind, bytes, mtime);
620 selection.admits(&Candidate {
621 relative: &relative,
622 name: &name,
623 kind,
624 bytes,
625 allocated: bytes.div_ceil(512) * 512,
626 mtime_ns: mtime,
627 ignored,
628 })
629 }
630
631 fn admits(selection: &Selection, path: &str, kind: EntryKind, bytes: u64, mtime: i64) -> bool {
632 classified_admits(selection, path, kind, bytes, mtime, false)
633 }
634
635 fn classified_admits(
636 selection: &Selection,
637 path: &str,
638 kind: EntryKind,
639 bytes: u64,
640 mtime: i64,
641 ignored: bool,
642 ) -> bool {
643 let (relative, name) = candidate(path, kind, bytes, mtime);
644 selection.admits(&Candidate {
645 relative: &relative,
646 name: &name,
647 kind,
648 bytes,
649 allocated: bytes.div_ceil(512) * 512,
650 mtime_ns: mtime,
651 ignored,
652 })
653 }
654
655 fn pattern(source: &str) -> Pattern {
656 Pattern::parse(source).expect("pattern compiles")
657 }
658
659 #[test]
660 fn a_default_selection_admits_everything_and_reads_the_fast_tier() {
661 let selection = Selection::default();
662 assert!(selection.is_unfiltered());
663 assert!(admits(&selection, "src/main.rs", EntryKind::File, 10, 5));
664 assert!(admits(&selection, "src", EntryKind::Dir, 0, 5));
665 }
666
667 #[test]
668 fn include_patterns_narrow_and_exclude_patterns_win() {
669 let mut selection = Selection { include: vec![pattern("*.rs")], ..Selection::default() };
670 assert!(!selection.is_unfiltered());
671 assert!(admits(&selection, "src/main.rs", EntryKind::File, 10, 5));
672 assert!(!admits(&selection, "src/main.toml", EntryKind::File, 10, 5));
673
674 selection.exclude.push(pattern("**/generated/**"));
677 assert!(!admits(&selection, "src/generated/api.rs", EntryKind::File, 10, 5));
678 assert!(admits(&selection, "src/hand/api.rs", EntryKind::File, 10, 5));
679 }
680
681 #[test]
682 fn min_size_follows_the_selected_metric() {
683 let apparent = Selection { min_size: Some(600), ..Selection::default() };
684 assert!(!admits(&apparent, "a.bin", EntryKind::File, 100, 0));
686
687 let allocated =
688 Selection { min_size: Some(600), size: SizeMetric::Allocated, ..Selection::default() };
689 assert!(admits(&allocated, "a.bin", EntryKind::File, 600, 0));
691 assert!(!admits(&allocated, "b.bin", EntryKind::File, 100, 0));
692 }
693
694 #[test]
695 fn the_modified_window_is_half_open() {
696 let selection = Selection {
697 modified: ModifiedWindow { since: Some(100), before: Some(200) },
698 ..Selection::default()
699 };
700 assert!(admits(&selection, "a", EntryKind::File, 1, 100));
703 assert!(admits(&selection, "b", EntryKind::File, 1, 150));
704 assert!(!admits(&selection, "c", EntryKind::File, 1, 200));
706 assert!(!admits(&selection, "d", EntryKind::File, 1, 99));
707 }
708
709 #[test]
710 fn kinds_filter_and_an_empty_list_means_every_kind() {
711 let files = Selection { kinds: vec![EntryKind::File], ..Selection::default() };
712 assert!(admits(&files, "a.rs", EntryKind::File, 1, 0));
713 assert!(!admits(&files, "src", EntryKind::Dir, 0, 0));
714
715 let both =
716 Selection { kinds: vec![EntryKind::File, EntryKind::Dir], ..Selection::default() };
717 assert!(admits(&both, "src", EntryKind::Dir, 0, 0));
718 assert!(!admits(&both, "link", EntryKind::Symlink, 0, 0));
719 }
720
721 #[test]
724 fn ignored_entries_select_one_partition_or_both() {
725 let include = Selection::default();
726 let exclude = Selection { ignored: IgnoredEntries::Exclude, ..Selection::default() };
727 let only = Selection { ignored: IgnoredEntries::Only, ..Selection::default() };
728 assert!(!exclude.is_unfiltered() && !only.is_unfiltered());
729 for (selection, admits_unignored, admits_ignored) in
730 [(&include, true, true), (&exclude, true, false), (&only, false, true)]
731 {
732 assert_eq!(
733 classified_admits(selection, "src/lib.rs", EntryKind::File, 1, 0, false),
734 admits_unignored,
735 "{:?} on an unignored entry",
736 selection.ignored
737 );
738 assert_eq!(
739 classified_admits(selection, "dist", EntryKind::Dir, 0, 0, true),
740 admits_ignored,
741 "{:?} on an ignored entry",
742 selection.ignored
743 );
744 }
745 for mode in [IgnoredEntries::Include, IgnoredEntries::Exclude, IgnoredEntries::Only] {
746 assert_eq!(IgnoredEntries::parse(mode.label()), Ok(mode));
747 }
748 assert_eq!(
749 IgnoredEntries::parse("some"),
750 Err("expected one of include, exclude, only".to_string())
751 );
752 }
753
754 #[test]
755 fn portable_catalog_predicates_compose_without_client_side_filtering() {
756 let selection = EntrySelection {
757 query: Selection { size: SizeMetric::Apparent, ..Selection::default() },
759 max_size: Some(10),
760 exclude_ignored: true,
761 terminal_extensions: vec![".rs".to_string(), ".md".to_string()],
762 ancestor_names: vec!["src".to_string(), "docs".to_string()],
763 ..EntrySelection::default()
764 };
765 assert!(entry_admits(&selection, "src/lib.rs", EntryKind::File, 10, 0, false));
766 assert!(entry_admits(&selection, "docs/readme.md", EntryKind::File, 9, 0, false));
767 assert!(!entry_admits(&selection, "src/lib.RS", EntryKind::File, 11, 0, false));
768 assert!(!entry_admits(&selection, "tests/lib.rs", EntryKind::File, 9, 0, false));
769 assert!(!entry_admits(&selection, "src/lib.rs", EntryKind::File, 9, 0, true));
770 assert!(!entry_admits(&selection, "src/.gitignore", EntryKind::File, 1, 0, false));
771 }
772
773 #[test]
774 fn logical_extensions_and_exact_names_form_one_name_identity_filter() {
775 let selection = EntrySelection {
776 logical_extensions: vec![".v2.zip".to_string()],
777 exact_names: vec!["makefile".to_string()],
778 ..EntrySelection::default()
779 };
780 assert!(entry_admits(&selection, "release.v2.zip", EntryKind::File, 1, 0, false));
781 assert!(entry_admits(&selection, "Makefile", EntryKind::File, 1, 0, false));
782 assert!(!entry_admits(&selection, "plain.zip", EntryKind::File, 1, 0, false));
783 assert!(!entry_admits(&selection, "README", EntryKind::File, 1, 0, false));
784 }
785
786 #[test]
787 fn terminal_extensions_and_ancestor_names_refuse_what_could_never_match() {
788 let refused = |hint: &str, outcome: Result<()>| match outcome {
789 Err(Error::InvalidValue { hint: actual, .. }) => {
790 assert!(actual.contains(hint), "{actual:?} names {hint:?}");
791 }
792 other => panic!("expected a refusal naming {hint:?}, got {other:?}"),
793 };
794 for (value, hint) in [
795 ("rs", "start with a dot"),
796 (".RS", "lowercase"),
797 (".Ée", "lowercase"),
798 (".", "canonical terminal suffixes"),
799 (".tar.gz", "canonical terminal suffixes"),
800 ("..", "canonical terminal suffixes"),
801 (".a/b", "canonical terminal suffixes"),
802 (".a\\b", "canonical terminal suffixes"),
803 ] {
804 let mut selection = EntrySelection::default();
805 refused(hint, selection.admit_terminal_extension(value));
806 assert!(selection.terminal_extensions.is_empty(), "{value:?} was not added");
807 let written = EntrySelection {
808 terminal_extensions: vec![value.to_string()],
809 ..Default::default()
810 };
811 refused(hint, written.validate());
812 }
813 for value in ["", ".", "..", "a/b", "a\\b"] {
814 let mut selection = EntrySelection::default();
815 refused("exact path-component names", selection.admit_ancestor_name(value));
816 let written =
817 EntrySelection { ancestor_names: vec![value.to_string()], ..Default::default() };
818 refused("exact path-component names", written.validate());
819 }
820
821 let mut selection = EntrySelection::default();
822 selection.admit_terminal_extension(".rs").expect("a canonical suffix");
823 selection.admit_terminal_extension(".c++").expect("a non-alphanumeric suffix");
824 refused("unique", selection.admit_terminal_extension(".rs"));
825 selection.admit_ancestor_name("src").expect("a component");
826 selection.admit_ancestor_name("x%FF").expect("an escaped component");
827 selection.admit_ancestor_name("..foo").expect("dots inside a name");
828 refused("unique", selection.admit_ancestor_name("src"));
829 assert_eq!(selection.terminal_extensions, [".rs", ".c++"]);
830 assert_eq!(selection.ancestor_names, ["src", "x%FF", "..foo"]);
831 selection.validate().expect("admitted values validate");
832 refused(
833 "unique",
834 EntrySelection {
835 terminal_extensions: vec![".md".to_string(), ".md".to_string()],
836 ..Default::default()
837 }
838 .validate(),
839 );
840 refused(
841 "unique",
842 EntrySelection {
843 ancestor_names: vec!["docs".to_string(), "docs".to_string()],
844 ..Default::default()
845 }
846 .validate(),
847 );
848 }
849
850 #[test]
854 fn a_list_wrong_twice_is_refused_in_the_catalog_query_order() {
855 let hint_of = |selection: EntrySelection| match selection.validate() {
856 Err(Error::InvalidValue { hint, .. }) => hint,
857 other => panic!("expected a refusal, got {other:?}"),
858 };
859 let terminal = |values: &[&str]| EntrySelection {
860 terminal_extensions: values.iter().map(ToString::to_string).collect(),
861 ..Default::default()
862 };
863 let ancestors = |values: &[&str]| EntrySelection {
864 ancestor_names: values.iter().map(ToString::to_string).collect(),
865 ..Default::default()
866 };
867 for (selection, hint) in [
868 (terminal(&["rs", "rs"]), TERMINAL_UNIQUE),
869 (terminal(&[".RS", ".RS"]), TERMINAL_UNIQUE),
870 (terminal(&[".RS", "rs"]), "terminal_extensions entries must start with a dot"),
871 (terminal(&[".tar.gz", ".RS"]), "terminal_extensions entries must be lowercase"),
872 (ancestors(&["..", ".."]), ANCESTOR_UNIQUE),
873 (
874 EntrySelection {
875 terminal_extensions: vec!["rs".to_string()],
876 ancestor_names: vec!["src".to_string(), "src".to_string()],
877 ..Default::default()
878 },
879 "terminal_extensions entries must start with a dot",
880 ),
881 ] {
882 assert_eq!(hint_of(selection), hint);
883 }
884 }
885
886 #[test]
887 fn bounds_admit_by_index_and_report_their_limit() {
888 assert!(Bound::All.admits(1_000_000));
889 assert_eq!(Bound::All.limit(), None);
890 assert!(Bound::Limit(2).admits(0));
891 assert!(Bound::Limit(2).admits(1));
892 assert!(!Bound::Limit(2).admits(2));
893 assert_eq!(Bound::Limit(2).limit(), Some(2));
894 assert!(!Bound::Limit(0).admits(0));
896 }
897
898 #[test]
899 fn an_unbounded_window_does_not_constrain() {
900 assert!(ModifiedWindow::default().is_unbounded());
901 assert!(ModifiedWindow::default().contains(i64::MIN));
902 assert!(ModifiedWindow::default().contains(i64::MAX));
903 }
904}