1use std::collections::HashSet;
16use std::path::{Path, PathBuf};
17use std::process::Command;
18
19use serde::Deserialize;
20
21#[derive(Debug, Clone, Deserialize)]
23#[serde(deny_unknown_fields)]
24#[allow(dead_code)]
25pub struct CaseMetadata {
26 pub name: String,
28 pub reason: String,
30 pub introduced_by: String,
32 pub topologies: Vec<String>,
34 pub from_range: Vec<String>,
36 pub to_range: Vec<String>,
38 pub features: Vec<String>,
40 pub owner: String,
42 #[serde(default)]
45 pub namespace: Option<String>,
46 #[serde(default)]
48 pub old_config: Option<OldConfigMetadata>,
49}
50
51#[derive(Debug, Clone, Deserialize)]
53#[serde(deny_unknown_fields)]
54pub struct OldConfigMetadata {
55 pub datanode: PathBuf,
57}
58
59impl CaseMetadata {
60 pub fn effective_namespace(&self, case_dir_name: &str) -> String {
63 self.namespace
64 .clone()
65 .unwrap_or_else(|| sanitize_namespace(case_dir_name))
66 }
67
68 pub fn old_datanode_overlay(&self) -> Option<&Path> {
70 self.old_config
71 .as_ref()
72 .map(|config| config.datanode.as_path())
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct CompatCase {
79 pub metadata: CaseMetadata,
81 pub dir: PathBuf,
83 pub namespace: String,
85}
86
87fn sanitize_namespace(name: &str) -> String {
89 let sanitized: String = name
90 .to_lowercase()
91 .chars()
92 .map(|c| {
93 if c.is_ascii_alphanumeric() || c == '_' {
94 c
95 } else {
96 '_'
97 }
98 })
99 .collect();
100
101 if sanitized
103 .chars()
104 .next()
105 .is_none_or(|c| !c.is_ascii_alphabetic())
106 {
107 format!("c_{sanitized}")
108 } else {
109 sanitized
110 }
111}
112
113pub fn discover_cases(case_root: &Path) -> Result<Vec<CompatCase>, String> {
118 let mut cases = Vec::new();
119
120 if !case_root.is_dir() {
121 return Err(format!(
122 "Case root directory not found: {}",
123 case_root.display()
124 ));
125 }
126
127 let entries = std::fs::read_dir(case_root)
128 .map_err(|e| format!("Failed to read case root {}: {e}", case_root.display()))?;
129
130 for entry in entries {
131 let entry = entry.map_err(|e| format!("Failed to read case dir entry: {e}"))?;
132 let path = entry.path();
133 if !path.is_dir() {
134 continue;
135 }
136
137 let case_toml_path = path.join("case.toml");
138 if !case_toml_path.is_file() {
139 println!("Skipping directory {}: no case.toml found", path.display());
140 continue;
141 }
142
143 let setup_sql = path.join("setup.sql");
144 let verify_sql = path.join("verify.sql");
145
146 for required in [&setup_sql, &verify_sql] {
147 if !required.is_file() {
148 return Err(format!(
149 "Missing required file {} in case directory {}",
150 required.display(),
151 path.display()
152 ));
153 }
154 }
155
156 let content = std::fs::read_to_string(&case_toml_path)
157 .map_err(|e| format!("Failed to read {}: {e}", case_toml_path.display()))?;
158
159 let metadata: CaseMetadata = toml::from_str(&content)
160 .map_err(|e| format!("Failed to parse {}: {e}", case_toml_path.display()))?;
161
162 let case_dir_name = path
163 .file_name()
164 .and_then(|n| n.to_str())
165 .unwrap_or("unknown");
166
167 let namespace = metadata.effective_namespace(case_dir_name);
168
169 cases.push(CompatCase {
170 metadata,
171 dir: path,
172 namespace,
173 });
174 }
175
176 if cases.is_empty() {
177 return Err(format!(
178 "No compat cases found under {}",
179 case_root.display()
180 ));
181 }
182
183 cases.sort_by(|a, b| a.dir.file_name().cmp(&b.dir.file_name()));
185
186 Ok(cases)
187}
188
189pub fn validate_cases_metadata(cases: &[CompatCase]) -> Result<(), String> {
198 for case in cases {
199 if !is_valid_namespace(&case.namespace) {
202 return Err(format!(
203 "Case '{}' has invalid namespace '{}': must match [a-z][a-z0-9_]*",
204 case.metadata.name, case.namespace
205 ));
206 }
207
208 if case.metadata.name.is_empty() {
210 return Err(format!("Case in {} has empty name", case.dir.display()));
211 }
212 if case.metadata.reason.is_empty() {
213 return Err(format!("Case '{}' has empty reason", case.metadata.name));
214 }
215 if case.metadata.introduced_by.is_empty() {
216 return Err(format!(
217 "Case '{}' has empty introduced_by",
218 case.metadata.name
219 ));
220 }
221 if case.metadata.owner.is_empty() {
222 return Err(format!("Case '{}' has empty owner", case.metadata.name));
223 }
224 if case.metadata.topologies.is_empty() {
225 return Err(format!(
226 "Case '{}' has empty topologies",
227 case.metadata.name
228 ));
229 }
230 if case.metadata.from_range.is_empty() {
231 return Err(format!(
232 "Case '{}' has empty from_range",
233 case.metadata.name
234 ));
235 }
236 if case.metadata.to_range.is_empty() {
237 return Err(format!("Case '{}' has empty to_range", case.metadata.name));
238 }
239 validate_version_constraints(&case.metadata.name, "from_range", &case.metadata.from_range)?;
240 validate_version_constraints(&case.metadata.name, "to_range", &case.metadata.to_range)?;
241 if case.metadata.features.is_empty() {
242 return Err(format!("Case '{}' has empty features", case.metadata.name));
243 }
244 }
245
246 Ok(())
247}
248
249pub fn validate_case_namespaces(cases: &[CompatCase]) -> Result<(), String> {
254 let mut namespaces: HashSet<&str> = HashSet::new();
255
256 for case in cases {
257 if !namespaces.insert(&case.namespace) {
258 return Err(format!(
259 "Duplicate namespace '{}' for case '{}'. \
260 Each case must have a unique effective namespace.",
261 case.namespace, case.metadata.name
262 ));
263 }
264 }
265
266 Ok(())
267}
268
269fn is_valid_namespace(s: &str) -> bool {
272 if s.is_empty() {
273 return false;
274 }
275 let mut chars = s.chars();
276 match chars.next() {
277 Some(c) if c.is_ascii_lowercase() => {}
278 _ => return false,
279 }
280 chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
281}
282
283fn validate_version_constraints(
284 case_name: &str,
285 field_name: &str,
286 constraints: &[String],
287) -> Result<(), String> {
288 for constraint in constraints {
289 parse_version_constraint(constraint).map_err(|e| {
290 format!("Case '{case_name}' has invalid {field_name} entry '{constraint}': {e}")
291 })?;
292 }
293 Ok(())
294}
295
296#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
302pub(crate) struct Version {
303 pub major: u64,
304 pub minor: u64,
305 pub patch: u64,
306}
307
308impl Version {
309 pub(crate) fn parse(raw: &str) -> Result<Self, String> {
311 let stripped = raw.strip_prefix('v').unwrap_or(raw);
312 let core = stripped
313 .split_once('-')
314 .map(|(core, _)| core)
315 .unwrap_or(stripped);
316 let core = core.split_once('+').map(|(core, _)| core).unwrap_or(core);
317 let parts: Vec<&str> = core.split('.').collect();
318 if parts.len() != 3 {
319 return Err(format!(
320 "Invalid version '{}': expected major.minor.patch",
321 raw
322 ));
323 }
324 let major = parts[0]
325 .parse::<u64>()
326 .map_err(|_| format!("Invalid major version in '{}'", raw))?;
327 let minor = parts[1]
328 .parse::<u64>()
329 .map_err(|_| format!("Invalid minor version in '{}'", raw))?;
330 let patch = parts[2]
331 .parse::<u64>()
332 .map_err(|_| format!("Invalid patch version in '{}'", raw))?;
333 Ok(Self {
334 major,
335 minor,
336 patch,
337 })
338 }
339}
340
341impl std::fmt::Display for Version {
342 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
343 write!(f, "v{}.{}.{}", self.major, self.minor, self.patch)
344 }
345}
346
347#[derive(Debug, Clone)]
349enum VersionConstraint {
350 Wildcard,
351 Exact(Version),
352 Gt(Version),
353 Gte(Version),
354 Lt(Version),
355 Lte(Version),
356}
357
358impl VersionConstraint {
359 fn matches(&self, version: &Version) -> bool {
361 match self {
362 Self::Wildcard => true,
363 Self::Exact(target) => version == target,
364 Self::Gt(target) => version > target,
365 Self::Gte(target) => version >= target,
366 Self::Lt(target) => version < target,
367 Self::Lte(target) => version <= target,
368 }
369 }
370}
371
372fn parse_version_constraint(raw: &str) -> Result<VersionConstraint, String> {
374 let trimmed = raw.trim();
375 if trimmed.is_empty() {
376 return Err("Empty version constraint".to_string());
377 }
378 if trimmed == "*" {
379 return Ok(VersionConstraint::Wildcard);
380 }
381
382 if let Some(ver_str) = trimmed.strip_prefix(">=") {
384 let ver = Version::parse(ver_str.trim())?;
385 return Ok(VersionConstraint::Gte(ver));
386 }
387 if let Some(ver_str) = trimmed.strip_prefix("<=") {
388 let ver = Version::parse(ver_str.trim())?;
389 return Ok(VersionConstraint::Lte(ver));
390 }
391 if let Some(ver_str) = trimmed.strip_prefix("==") {
392 let ver = Version::parse(ver_str.trim())?;
393 return Ok(VersionConstraint::Exact(ver));
394 }
395 if let Some(ver_str) = trimmed.strip_prefix('=') {
396 let ver = Version::parse(ver_str.trim())?;
397 return Ok(VersionConstraint::Exact(ver));
398 }
399 if let Some(ver_str) = trimmed.strip_prefix('>') {
400 let ver = Version::parse(ver_str.trim())?;
401 return Ok(VersionConstraint::Gt(ver));
402 }
403 if let Some(ver_str) = trimmed.strip_prefix('<') {
404 let ver = Version::parse(ver_str.trim())?;
405 return Ok(VersionConstraint::Lt(ver));
406 }
407
408 let ver = Version::parse(trimmed)?;
410 Ok(VersionConstraint::Exact(ver))
411}
412
413pub(crate) fn version_matches_range(version: Option<&Version>, constraints: &[String]) -> bool {
423 if constraints.is_empty() {
424 return false;
425 }
426
427 for raw in constraints {
428 match parse_version_constraint(raw) {
429 Ok(VersionConstraint::Wildcard) => return true,
430 Ok(constraint) => {
431 if version.is_some_and(|ver| constraint.matches(ver)) {
432 return true;
433 }
434 }
436 Err(e) => {
437 println!(
438 "Warning: invalid version constraint '{}' — skipping this entry: {e}",
439 raw
440 );
441 }
442 }
443 }
444
445 false
446}
447
448pub(crate) fn try_infer_version(bins_dir: &Path) -> Option<Version> {
451 let binary = bins_dir.join("greptime");
452 if !binary.is_file() {
453 return None;
454 }
455 let output = Command::new(&binary).arg("--version").output().ok()?;
456 if !output.status.success() {
457 return None;
458 }
459 let stdout = String::from_utf8_lossy(&output.stdout);
460 for token in stdout.split_whitespace() {
463 let candidate = token.trim();
465 let looks_like_version = candidate.starts_with('v')
466 || candidate.chars().next().is_some_and(|c| c.is_ascii_digit());
467 let parsed_version = looks_like_version
468 .then(|| Version::parse(candidate).ok())
469 .flatten();
470 if let Some(ver) = parsed_version {
471 return Some(ver);
472 }
473 }
474 None
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480
481 #[test]
482 fn test_sanitize_namespace() {
483 assert_eq!(sanitize_namespace("basic_table"), "basic_table");
484 assert_eq!(sanitize_namespace("my-case"), "my_case");
485 assert_eq!(sanitize_namespace("123abc"), "c_123abc");
486 assert_eq!(sanitize_namespace("UPPER"), "upper");
487 assert_eq!(sanitize_namespace("a.b-c"), "a_b_c");
488 }
489
490 #[test]
491 fn test_case_metadata_parses_old_datanode_overlay() {
492 let metadata: CaseMetadata = toml::from_str(
493 r#"
494 name = "case"
495 reason = "reason"
496 introduced_by = "test"
497 topologies = ["distributed"]
498 from_range = ["*"]
499 to_range = ["*"]
500 features = ["table"]
501 owner = "team"
502
503 [old_config]
504 datanode = "old-datanode.overlay.toml"
505 "#,
506 )
507 .unwrap();
508
509 assert_eq!(
510 metadata.old_datanode_overlay(),
511 Some(Path::new("old-datanode.overlay.toml"))
512 );
513 }
514
515 #[test]
516 fn test_case_metadata_rejects_empty_old_config() {
517 let error = toml::from_str::<CaseMetadata>(
518 r#"
519 name = "case"
520 reason = "reason"
521 introduced_by = "test"
522 topologies = ["distributed"]
523 from_range = ["*"]
524 to_range = ["*"]
525 features = ["table"]
526 owner = "team"
527
528 [old_config]
529 "#,
530 )
531 .unwrap_err();
532
533 assert!(error.to_string().contains("missing field `datanode`"));
534 }
535
536 #[test]
537 fn test_case_metadata_rejects_unknown_old_config_fields() {
538 let error = toml::from_str::<CaseMetadata>(
539 r#"
540 name = "case"
541 reason = "reason"
542 introduced_by = "test"
543 topologies = ["distributed"]
544 from_range = ["*"]
545 to_range = ["*"]
546 features = ["table"]
547 owner = "team"
548
549 [old_config]
550 unsupported = "value"
551 "#,
552 )
553 .unwrap_err();
554
555 assert!(error.to_string().contains("unknown field `unsupported`"));
556 }
557
558 #[test]
559 fn test_validate_cases_metadata_rejects_empty_required_vectors() {
560 let case = CompatCase {
561 metadata: CaseMetadata {
562 name: "case".to_string(),
563 reason: "reason".to_string(),
564 introduced_by: "pr".to_string(),
565 topologies: vec![],
566 from_range: vec!["*".to_string()],
567 to_range: vec!["*".to_string()],
568 features: vec!["table".to_string()],
569 owner: "team".to_string(),
570 namespace: None,
571 old_config: None,
572 },
573 dir: PathBuf::from("case"),
574 namespace: "case".to_string(),
575 };
576
577 assert!(validate_cases_metadata(&[case]).is_err());
578 }
579
580 #[test]
581 fn test_validate_cases_metadata_catches_invalid_version_constraint() {
582 let case = CompatCase {
583 metadata: CaseMetadata {
584 name: "bad_constraint".to_string(),
585 reason: "test".to_string(),
586 introduced_by: "test".to_string(),
587 topologies: vec!["distributed".to_string()],
588 from_range: vec![">=not-a-version".to_string()],
589 to_range: vec!["*".to_string()],
590 features: vec!["table".to_string()],
591 owner: "test".to_string(),
592 namespace: None,
593 old_config: None,
594 },
595 dir: PathBuf::from("bad_constraint"),
596 namespace: "bad_constraint".to_string(),
597 };
598
599 assert!(validate_cases_metadata(&[case]).is_err());
600 }
601
602 #[test]
603 fn test_validate_case_namespaces_rejects_duplicate() {
604 let case_a = CompatCase {
605 metadata: CaseMetadata {
606 name: "case_a".to_string(),
607 reason: "test".to_string(),
608 introduced_by: "test".to_string(),
609 topologies: vec!["distributed".to_string()],
610 from_range: vec!["*".to_string()],
611 to_range: vec!["*".to_string()],
612 features: vec!["table".to_string()],
613 owner: "test".to_string(),
614 namespace: None,
615 old_config: None,
616 },
617 dir: PathBuf::from("case_a"),
618 namespace: "shared_name".to_string(),
619 };
620 let case_b = CompatCase {
621 metadata: CaseMetadata {
622 name: "case_b".to_string(),
623 reason: "test".to_string(),
624 introduced_by: "test".to_string(),
625 topologies: vec!["distributed".to_string()],
626 from_range: vec!["*".to_string()],
627 to_range: vec!["*".to_string()],
628 features: vec!["table".to_string()],
629 owner: "test".to_string(),
630 namespace: None,
631 old_config: None,
632 },
633 dir: PathBuf::from("case_b"),
634 namespace: "shared_name".to_string(),
635 };
636
637 assert!(validate_cases_metadata(&[case_a.clone(), case_b.clone()]).is_ok());
638 assert!(validate_case_namespaces(&[case_a, case_b]).is_err());
639 }
640
641 #[test]
642 fn test_validate_case_namespaces_rejects_any_duplicate() {
643 let case_a = CompatCase {
646 metadata: CaseMetadata {
647 name: "case_a".to_string(),
648 reason: "test".to_string(),
649 introduced_by: "test".to_string(),
650 topologies: vec!["distributed".to_string()],
651 from_range: vec!["*".to_string()],
652 to_range: vec!["*".to_string()],
653 features: vec!["table".to_string()],
654 owner: "test".to_string(),
655 namespace: Some("shared_name".to_string()),
656 old_config: None,
657 },
658 dir: PathBuf::from("case_a"),
659 namespace: "shared_name".to_string(),
660 };
661 let case_b = CompatCase {
662 metadata: CaseMetadata {
663 name: "case_b".to_string(),
664 reason: "test".to_string(),
665 introduced_by: "test".to_string(),
666 topologies: vec!["distributed".to_string()],
667 from_range: vec!["*".to_string()],
668 to_range: vec!["*".to_string()],
669 features: vec!["table".to_string()],
670 owner: "test".to_string(),
671 namespace: Some("shared_name".to_string()),
672 old_config: None,
673 },
674 dir: PathBuf::from("case_b"),
675 namespace: "shared_name".to_string(),
676 };
677
678 assert!(validate_case_namespaces(&[case_a, case_b]).is_err());
679 }
680
681 #[test]
682 fn test_validate_cases_metadata_rejects_invalid_version_range() {
683 let case = CompatCase {
684 metadata: CaseMetadata {
685 name: "case".to_string(),
686 reason: "reason".to_string(),
687 introduced_by: "pr".to_string(),
688 topologies: vec!["distributed".to_string()],
689 from_range: vec![">=not-a-version".to_string()],
690 to_range: vec!["*".to_string()],
691 features: vec!["table".to_string()],
692 owner: "team".to_string(),
693 namespace: None,
694 old_config: None,
695 },
696 dir: PathBuf::from("case"),
697 namespace: "case".to_string(),
698 };
699
700 assert!(validate_cases_metadata(&[case]).is_err());
701 }
702
703 fn write_minimal_case(dir: &Path) {
705 std::fs::create_dir_all(dir).unwrap();
706 let case_toml = dir.join("case.toml");
707 let setup_sql = dir.join("setup.sql");
708 let verify_sql = dir.join("verify.sql");
709
710 std::fs::write(
711 &case_toml,
712 r#"
713name = "test_case"
714reason = "test"
715introduced_by = "test"
716topologies = ["distributed"]
717from_range = ["*"]
718to_range = ["*"]
719features = ["table"]
720owner = "test"
721"#,
722 )
723 .unwrap();
724 std::fs::write(&setup_sql, "CREATE TABLE t (a INT);").unwrap();
725 std::fs::write(&verify_sql, "SELECT * FROM t;").unwrap();
726 }
727
728 #[test]
729 fn test_discover_cases_allows_missing_verify_result() {
730 let tmp = tempfile::tempdir().unwrap();
731 let case_dir = tmp.path().join("my_case");
732 write_minimal_case(&case_dir);
733 assert!(!case_dir.join("verify.result").is_file());
735
736 let cases =
737 discover_cases(tmp.path()).expect("discover should succeed without verify.result");
738 assert_eq!(cases.len(), 1);
739 assert_eq!(cases[0].metadata.name, "test_case");
740 }
741
742 #[test]
743 fn test_discover_cases_rejects_missing_setup_sql() {
744 let tmp = tempfile::tempdir().unwrap();
745 let case_dir = tmp.path().join("my_case");
746 write_minimal_case(&case_dir);
747 std::fs::remove_file(case_dir.join("setup.sql")).unwrap();
748
749 assert!(discover_cases(tmp.path()).is_err());
750 }
751
752 #[test]
753 fn test_discover_cases_rejects_missing_verify_sql() {
754 let tmp = tempfile::tempdir().unwrap();
755 let case_dir = tmp.path().join("my_case");
756 write_minimal_case(&case_dir);
757 std::fs::remove_file(case_dir.join("verify.sql")).unwrap();
758
759 assert!(discover_cases(tmp.path()).is_err());
760 }
761
762 #[test]
763 fn test_discover_cases_rejects_unknown_isolation_field() {
764 let tmp = tempfile::tempdir().unwrap();
765 let case_dir = tmp.path().join("my_case");
766 write_minimal_case(&case_dir);
767 std::fs::write(
768 case_dir.join("case.toml"),
769 r#"
770name = "test_case"
771reason = "test"
772introduced_by = "test"
773topologies = ["distributed"]
774from_range = ["*"]
775to_range = ["*"]
776features = ["table"]
777owner = "test"
778isolation = "shared"
779"#,
780 )
781 .unwrap();
782
783 let err = discover_cases(tmp.path()).unwrap_err();
784 assert!(err.contains("unknown field `isolation`"));
785 }
786
787 #[test]
788 fn test_discover_cases_rejects_missing_case_toml() {
789 let tmp = tempfile::tempdir().unwrap();
790 let case_dir = tmp.path().join("my_case");
791 write_minimal_case(&case_dir);
792 std::fs::remove_file(case_dir.join("case.toml")).unwrap();
793
794 let result = discover_cases(tmp.path());
796 assert!(result.is_err()); }
798
799 fn mkver(s: &str) -> Version {
804 Version::parse(s).unwrap()
805 }
806
807 #[test]
808 fn test_parse_version_constraint_wildcard() {
809 let c = parse_version_constraint("*").unwrap();
810 assert!(matches!(c, VersionConstraint::Wildcard));
811 }
812
813 #[test]
814 fn test_parse_version_constraint_exact() {
815 let c = parse_version_constraint("v1.2.3").unwrap();
816 assert!(matches!(c, VersionConstraint::Exact(ref v) if v == &mkver("v1.2.3")));
817 }
818
819 #[test]
820 fn test_parse_version_constraint_gte() {
821 let c = parse_version_constraint(">=v1.1.0").unwrap();
822 assert!(matches!(c, VersionConstraint::Gte(ref v) if v == &mkver("v1.1.0")));
823 }
824
825 #[test]
826 fn test_parse_version_constraint_lte() {
827 let c = parse_version_constraint("<=v1.1.0").unwrap();
828 assert!(matches!(c, VersionConstraint::Lte(ref v) if v == &mkver("v1.1.0")));
829 }
830
831 #[test]
832 fn test_parse_version_constraint_gt() {
833 let c = parse_version_constraint(">v1.0.0").unwrap();
834 assert!(matches!(c, VersionConstraint::Gt(ref v) if v == &mkver("v1.0.0")));
835 }
836
837 #[test]
838 fn test_parse_version_constraint_lt() {
839 let c = parse_version_constraint("<v2.0.0").unwrap();
840 assert!(matches!(c, VersionConstraint::Lt(ref v) if v == &mkver("v2.0.0")));
841 }
842
843 #[test]
844 fn test_parse_version_constraint_eq_double() {
845 let c = parse_version_constraint("==v1.0.0").unwrap();
846 assert!(matches!(c, VersionConstraint::Exact(ref v) if v == &mkver("v1.0.0")));
847 }
848
849 #[test]
850 fn test_version_matches_range_wildcard() {
851 assert!(version_matches_range(None, &["*".to_string()]));
852 assert!(version_matches_range(
853 Some(&mkver("v9.9.9")),
854 &["*".to_string()]
855 ));
856 }
857
858 #[test]
859 fn test_version_matches_range_legacy_jsonb() {
860 let from_range = vec!["<=v1.1.0".to_string()];
861 let to_range = vec![">=v1.1.1".to_string()];
862
863 assert!(version_matches_range(Some(&mkver("v0.9.5")), &from_range));
865 assert!(version_matches_range(Some(&mkver("v1.1.0")), &from_range));
866 assert!(!version_matches_range(Some(&mkver("v1.1.1")), &from_range));
867 assert!(!version_matches_range(Some(&mkver("v1.2.0")), &from_range));
868
869 assert!(version_matches_range(Some(&mkver("v1.1.1")), &to_range));
871 assert!(version_matches_range(Some(&mkver("v2.0.0")), &to_range));
872 assert!(!version_matches_range(Some(&mkver("v1.1.0")), &to_range));
873 assert!(!version_matches_range(Some(&mkver("v0.9.5")), &to_range));
874 }
875
876 #[test]
877 fn test_version_matches_range_unknown_version() {
878 assert!(!version_matches_range(None, &["<=v1.1.0".to_string()]));
880 assert!(!version_matches_range(None, &[">=v1.1.1".to_string()]));
881 assert!(!version_matches_range(None, &["v1.0.0".to_string()]));
882 assert!(version_matches_range(None, &["*".to_string()]));
884 }
885
886 #[test]
887 fn test_version_matches_range_exact() {
888 let range = vec!["v1.0.0".to_string(), "v2.0.0".to_string()];
889 assert!(version_matches_range(Some(&mkver("v1.0.0")), &range));
890 assert!(version_matches_range(Some(&mkver("v2.0.0")), &range));
891 assert!(!version_matches_range(Some(&mkver("v1.5.0")), &range));
892 }
893
894 #[test]
895 fn test_version_parse_without_v() {
896 let ver = Version::parse("1.2.3").unwrap();
897 assert_eq!(ver.major, 1);
898 assert_eq!(ver.minor, 2);
899 assert_eq!(ver.patch, 3);
900 }
901
902 #[test]
903 fn test_version_parse_with_suffix() {
904 let ver = Version::parse("1.2.3-alpha+build").unwrap();
905 assert_eq!(ver.major, 1);
906 assert_eq!(ver.minor, 2);
907 assert_eq!(ver.patch, 3);
908 }
909
910 #[test]
911 fn test_try_infer_version_no_binary() {
912 let tmp = tempfile::tempdir().unwrap();
913 assert!(try_infer_version(tmp.path()).is_none());
914 }
915}