1use std::collections::BTreeMap;
2use std::fs::{self};
3use std::io::Write;
4use std::ops::{Deref, DerefMut};
5use std::path::{Path, PathBuf};
6use std::{env, str};
7
8use semver::{Version, VersionReq};
9use termcolor::{BufferWriter, Color, ColorSpec, WriteColor};
10
11use super::dependency::Dependency;
12use super::errors::*;
13
14const MANIFEST_FILENAME: &str = "Cargo.toml";
15const DEP_TABLES: &[&str] = &["dependencies", "dev-dependencies", "build-dependencies"];
16
17#[derive(Debug, Clone)]
19pub struct Manifest {
20 pub data: toml_edit::Document,
22}
23
24impl Manifest {
25 pub fn package_name(&self) -> CargoResult<&str> {
27 self.data
28 .as_table()
29 .get("package")
30 .and_then(|m| m["name"].as_str())
31 .ok_or_else(parse_manifest_err)
32 }
33
34 pub fn get_table<'a>(&'a self, table_path: &[String]) -> CargoResult<&'a toml_edit::Item> {
36 fn descend<'a>(
38 input: &'a toml_edit::Item,
39 path: &[String],
40 ) -> CargoResult<&'a toml_edit::Item> {
41 if let Some(segment) = path.get(0) {
42 let value = input
43 .get(&segment)
44 .ok_or_else(|| non_existent_table_err(segment))?;
45
46 if value.is_table_like() {
47 descend(value, &path[1..])
48 } else {
49 Err(non_existent_table_err(segment))
50 }
51 } else {
52 Ok(input)
53 }
54 }
55
56 descend(self.data.as_item(), table_path)
57 }
58
59 pub fn get_table_mut<'a>(
61 &'a mut self,
62 table_path: &[String],
63 ) -> CargoResult<&'a mut toml_edit::Item> {
64 fn descend<'a>(
66 input: &'a mut toml_edit::Item,
67 path: &[String],
68 ) -> CargoResult<&'a mut toml_edit::Item> {
69 if let Some(segment) = path.get(0) {
70 let value = input[&segment].or_insert(toml_edit::table());
71
72 if value.is_table_like() {
73 descend(value, &path[1..])
74 } else {
75 Err(non_existent_table_err(segment))
76 }
77 } else {
78 Ok(input)
79 }
80 }
81
82 descend(self.data.as_item_mut(), table_path)
83 }
84
85 pub fn get_sections(&self) -> Vec<(Vec<String>, toml_edit::Item)> {
88 let mut sections = Vec::new();
89
90 for dependency_type in DEP_TABLES {
91 if self
93 .data
94 .get(dependency_type)
95 .map(|t| t.is_table_like())
96 .unwrap_or(false)
97 {
98 sections.push((
99 vec![String::from(*dependency_type)],
100 self.data[dependency_type].clone(),
101 ))
102 }
103
104 let target_sections = self
106 .data
107 .as_table()
108 .get("target")
109 .and_then(toml_edit::Item::as_table_like)
110 .into_iter()
111 .flat_map(toml_edit::TableLike::iter)
112 .filter_map(|(target_name, target_table)| {
113 let dependency_table = target_table.get(dependency_type)?;
114 dependency_table.as_table_like().map(|_| {
115 (
116 vec![
117 "target".to_string(),
118 target_name.to_string(),
119 String::from(*dependency_type),
120 ],
121 dependency_table.clone(),
122 )
123 })
124 });
125
126 sections.extend(target_sections);
127 }
128
129 sections
130 }
131
132 pub fn features(&self) -> CargoResult<BTreeMap<String, Vec<String>>> {
134 let mut features: BTreeMap<String, Vec<String>> = match self.data.as_table().get("features")
135 {
136 None => BTreeMap::default(),
137 Some(item) => match item {
138 toml_edit::Item::None => BTreeMap::default(),
139 toml_edit::Item::Table(t) => t
140 .iter()
141 .map(|(k, v)| {
142 let k = k.to_owned();
143 let v = v
144 .as_array()
145 .cloned()
146 .unwrap_or_default()
147 .iter()
148 .map(|v| v.as_str().map(|s| s.to_owned()))
149 .collect::<Option<Vec<_>>>();
150 v.map(|v| (k, v))
151 })
152 .collect::<Option<_>>()
153 .ok_or_else(invalid_cargo_config)?,
154 _ => return Err(invalid_cargo_config()),
155 },
156 };
157
158 let sections = self.get_sections();
159 for (_, deps) in sections {
160 features.extend(
161 deps.as_table_like()
162 .unwrap()
163 .iter()
164 .filter_map(|(key, dep_item)| {
165 let table = dep_item.as_table_like()?;
166 table
167 .get("optional")
168 .and_then(|o| o.as_value())
169 .and_then(|o| o.as_bool())
170 .unwrap_or(false)
171 .then(|| (key.to_owned(), vec![]))
172 }),
173 );
174 }
175
176 Ok(features)
177 }
178}
179
180impl str::FromStr for Manifest {
181 type Err = Error;
182
183 fn from_str(input: &str) -> ::std::result::Result<Self, Self::Err> {
185 let d: toml_edit::Document = input.parse().with_context(|| "Manifest not valid TOML")?;
186
187 Ok(Manifest { data: d })
188 }
189}
190
191impl std::fmt::Display for Manifest {
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 let s = self.data.to_string();
194 s.fmt(f)
195 }
196}
197
198#[derive(Debug)]
200pub struct LocalManifest {
201 pub path: PathBuf,
203 pub manifest: Manifest,
205}
206
207impl Deref for LocalManifest {
208 type Target = Manifest;
209
210 fn deref(&self) -> &Manifest {
211 &self.manifest
212 }
213}
214
215impl DerefMut for LocalManifest {
216 fn deref_mut(&mut self) -> &mut Manifest {
217 &mut self.manifest
218 }
219}
220
221impl LocalManifest {
222 pub fn find(path: Option<&Path>) -> CargoResult<Self> {
225 let path = dunce::canonicalize(find(path)?)?;
226 Self::try_new(&path)
227 }
228
229 pub fn try_new(path: &Path) -> CargoResult<Self> {
231 let path = path.to_path_buf();
232 let data =
233 std::fs::read_to_string(&path).with_context(|| "Failed to read manifest contents")?;
234 let manifest = data.parse().with_context(|| "Unable to parse Cargo.toml")?;
235 Ok(LocalManifest { manifest, path })
236 }
237
238 pub fn write(&self) -> CargoResult<()> {
240 if !self.manifest.data.contains_key("package")
241 && !self.manifest.data.contains_key("project")
242 {
243 if self.manifest.data.contains_key("workspace") {
244 anyhow::bail!(
245 "Found virtual manifest at {}, but this command requires running against an \
246 actual package in this workspace.",
247 self.path.display()
248 );
249 } else {
250 anyhow::bail!(
251 "Missing expected `package` or `project` fields in {}",
252 self.path.display()
253 );
254 }
255 }
256
257 let s = self.manifest.data.to_string();
258 let new_contents_bytes = s.as_bytes();
259
260 std::fs::write(&self.path, new_contents_bytes)
261 .with_context(|| "Failed to write updated Cargo.toml")
262 }
263
264 pub fn upgrade(
267 &mut self,
268 dependency: &Dependency,
269 dry_run: bool,
270 skip_compatible: bool,
271 ) -> CargoResult<()> {
272 for (table_path, table) in self.get_sections() {
273 let table_like = table.as_table_like().expect("Unexpected non-table");
274 for (name, toml_item) in table_like.iter() {
275 let dep_name = toml_item
276 .as_table_like()
277 .and_then(|t| t.get("package").and_then(|p| p.as_str()))
278 .unwrap_or(name);
279 if dep_name == dependency.name {
280 if skip_compatible {
281 let old_version = get_version(toml_item)?;
282 if old_version_compatible(dependency, old_version)? {
283 continue;
284 }
285 }
286 self.update_table_named_entry(&table_path, name, dependency, dry_run)?;
287 }
288 }
289 }
290
291 self.write()
292 }
293
294 pub fn get_dependency(&self, table_path: &[String], dep_key: &str) -> CargoResult<Dependency> {
296 let crate_root = self.path.parent().expect("manifest path is absolute");
297 let table = self.get_table(table_path)?;
298 let table = table
299 .as_table_like()
300 .ok_or_else(|| non_existent_table_err(table_path.join(".")))?;
301 let dep_item = table
302 .get(dep_key)
303 .ok_or_else(|| non_existent_dependency_err(dep_key, table_path.join(".")))?;
304 Dependency::from_toml(crate_root, dep_key, dep_item).ok_or_else(|| {
305 anyhow::format_err!("Invalid dependency {}.{}", table_path.join("."), dep_key)
306 })
307 }
308
309 pub fn get_dependencies(
311 &self,
312 ) -> impl Iterator<Item = (Vec<String>, CargoResult<Dependency>)> + '_ {
313 self.filter_dependencies(|_| true)
314 }
315
316 pub fn get_dependency_versions<'s>(
318 &'s self,
319 dep_key: &'s str,
320 ) -> impl Iterator<Item = (Vec<String>, CargoResult<Dependency>)> + 's {
321 self.filter_dependencies(move |key| key == dep_key)
322 }
323
324 fn filter_dependencies<'s, P>(
325 &'s self,
326 mut predicate: P,
327 ) -> impl Iterator<Item = (Vec<String>, CargoResult<Dependency>)> + 's
328 where
329 P: FnMut(&str) -> bool + 's,
330 {
331 let crate_root = self.path.parent().expect("manifest path is absolute");
332 self.get_sections()
333 .into_iter()
334 .filter_map(move |(table_path, table)| {
335 let table = table.into_table().ok()?;
336 Some(
337 table
338 .into_iter()
339 .filter_map(|(key, item)| {
340 if predicate(&key) {
341 Some((table_path.clone(), key, item))
342 } else {
343 None
344 }
345 })
346 .collect::<Vec<_>>(),
347 )
348 })
349 .flatten()
350 .map(move |(table_path, dep_key, dep_item)| {
351 let dep = Dependency::from_toml(crate_root, &dep_key, &dep_item);
352 match dep {
353 Some(dep) => (table_path, Ok(dep)),
354 None => {
355 let message = anyhow::format_err!(
356 "Invalid dependency {}.{}",
357 table_path.join("."),
358 dep_key
359 );
360 (table_path, Err(message))
361 }
362 }
363 })
364 }
365
366 pub fn insert_into_table(
368 &mut self,
369 table_path: &[String],
370 dep: &Dependency,
371 ) -> CargoResult<()> {
372 let crate_root = self
373 .path
374 .parent()
375 .expect("manifest path is absolute")
376 .to_owned();
377 let dep_key = dep.toml_key();
378
379 let table = self.get_table_mut(table_path)?;
380 if let Some(dep_item) = table.as_table_like_mut().unwrap().get_mut(dep_key) {
381 dep.update_toml(&crate_root, dep_item);
382 } else {
383 let new_dependency = dep.to_toml(&crate_root);
384 table[dep_key] = new_dependency;
385 }
386 if let Some(t) = table.as_inline_table_mut() {
387 t.fmt()
388 }
389
390 Ok(())
391 }
392
393 pub fn update_table_entry(
395 &mut self,
396 table_path: &[String],
397 dep: &Dependency,
398 dry_run: bool,
399 ) -> CargoResult<()> {
400 self.update_table_named_entry(table_path, dep.toml_key(), dep, dry_run)
401 }
402
403 pub fn update_table_named_entry(
405 &mut self,
406 table_path: &[String],
407 dep_key: &str,
408 dep: &Dependency,
409 dry_run: bool,
410 ) -> CargoResult<()> {
411 let crate_root = self
412 .path
413 .parent()
414 .expect("manifest path is absolute")
415 .to_owned();
416 let table = self.get_table_mut(table_path)?;
417
418 if table.as_table_like().unwrap().contains_key(dep_key) {
420 let new_dependency = dep.to_toml(&crate_root);
421
422 if let Err(e) = print_upgrade_if_necessary(&dep.name, &table[dep_key], &new_dependency)
423 {
424 eprintln!("Error while displaying upgrade message, {}", e);
425 }
426 if !dry_run {
427 dep.update_toml(&crate_root, &mut table[dep_key]);
428 if let Some(t) = table.as_inline_table_mut() {
429 t.fmt()
430 }
431 }
432 }
433
434 Ok(())
435 }
436
437 pub fn remove_from_table(&mut self, table: &str, name: &str) -> CargoResult<()> {
455 let parent_table = self
456 .data
457 .get_mut(table)
458 .filter(|t| t.is_table_like())
459 .ok_or_else(|| non_existent_table_err(table))?;
460
461 {
462 let dep = parent_table
463 .get_mut(name)
464 .filter(|t| !t.is_none())
465 .ok_or_else(|| non_existent_dependency_err(name, table))?;
466 *dep = toml_edit::Item::None;
468 }
469
470 if parent_table.as_table_like().unwrap().is_empty() {
472 *parent_table = toml_edit::Item::None;
473 }
474
475 Ok(())
476 }
477
478 pub fn add_deps(&mut self, table: &[String], deps: &[Dependency]) -> CargoResult<()> {
480 deps.iter()
481 .map(|dep| self.insert_into_table(table, dep))
482 .collect::<CargoResult<Vec<_>>>()?;
483
484 Ok(())
485 }
486
487 pub fn get_dependency_tables_mut<'r>(
489 &'r mut self,
490 ) -> impl Iterator<Item = &mut dyn toml_edit::TableLike> + 'r {
491 let root = self.data.as_table_mut();
492 root.iter_mut().flat_map(|(k, v)| {
493 if DEP_TABLES.contains(&k.get()) {
494 v.as_table_like_mut().into_iter().collect::<Vec<_>>()
495 } else if k == "target" {
496 v.as_table_like_mut()
497 .unwrap()
498 .iter_mut()
499 .flat_map(|(_, v)| {
500 v.as_table_like_mut().into_iter().flat_map(|v| {
501 v.iter_mut().filter_map(|(k, v)| {
502 if DEP_TABLES.contains(&k.get()) {
503 v.as_table_like_mut()
504 } else {
505 None
506 }
507 })
508 })
509 })
510 .collect::<Vec<_>>()
511 } else {
512 Vec::new()
513 }
514 })
515 }
516
517 pub fn set_package_version(&mut self, version: &Version) {
519 self.data["package"]["version"] = toml_edit::value(version.to_string());
520 }
521
522 pub fn gc_dep(&mut self, dep_key: &str) {
524 let status = self.dep_feature(dep_key);
525 if matches!(status, FeatureStatus::None | FeatureStatus::DepFeature) {
526 if let toml_edit::Item::Table(feature_table) = &mut self.data.as_table_mut()["features"]
527 {
528 for (_feature, mut activated_crates) in feature_table.iter_mut() {
529 if let toml_edit::Item::Value(toml_edit::Value::Array(feature_activations)) =
530 &mut activated_crates
531 {
532 remove_feature_activation(feature_activations, dep_key, status);
533 }
534 }
535 }
536 }
537 }
538
539 fn dep_feature(&self, dep_key: &str) -> FeatureStatus {
540 let mut status = FeatureStatus::None;
541 for (_, tbl) in self.get_sections() {
542 if let toml_edit::Item::Table(tbl) = tbl {
543 if let Some(dep_item) = tbl.get(dep_key) {
544 let optional = dep_item.get("optional");
545 let optional = optional.and_then(|i| i.as_value());
546 let optional = optional.and_then(|i| i.as_bool());
547 let optional = optional.unwrap_or(false);
548 if optional {
549 return FeatureStatus::Feature;
550 } else {
551 status = FeatureStatus::DepFeature;
552 }
553 }
554 }
555 }
556 status
557 }
558}
559
560#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
561enum FeatureStatus {
562 None,
563 DepFeature,
564 Feature,
565}
566
567fn remove_feature_activation(
568 feature_activations: &mut toml_edit::Array,
569 dep: &str,
570 status: FeatureStatus,
571) {
572 let dep_feature: &str = &format!("{}/", dep);
573
574 let remove_list: Vec<usize> = feature_activations
575 .iter()
576 .enumerate()
577 .filter_map(|(idx, feature_activation)| {
578 if let toml_edit::Value::String(feature_activation) = feature_activation {
579 let activation = feature_activation.value();
580 match status {
581 FeatureStatus::None => activation == dep || activation.starts_with(dep_feature),
582 FeatureStatus::DepFeature => activation == dep,
583 FeatureStatus::Feature => false,
584 }
585 .then(|| idx)
586 } else {
587 None
588 }
589 })
590 .collect();
591
592 for idx in remove_list.iter().rev() {
594 feature_activations.remove(*idx);
595 }
596}
597
598pub fn find(specified: Option<&Path>) -> CargoResult<PathBuf> {
604 match specified {
605 Some(path)
606 if fs::metadata(&path)
607 .with_context(|| "Failed to get cargo file metadata")?
608 .is_file() =>
609 {
610 Ok(path.to_owned())
611 }
612 Some(path) => search(path),
613 None => search(&env::current_dir().with_context(|| "Failed to get current directory")?),
614 }
615}
616
617fn search(dir: &Path) -> CargoResult<PathBuf> {
619 let mut current_dir = dir;
620
621 loop {
622 let manifest = current_dir.join(MANIFEST_FILENAME);
623 if fs::metadata(&manifest).is_ok() {
624 return Ok(manifest);
625 }
626
627 current_dir = match current_dir.parent() {
628 Some(current_dir) => current_dir,
629 None => {
630 anyhow::bail!("Unable to find Cargo.toml for {}", dir.display());
631 }
632 };
633 }
634}
635
636fn get_version(old_dep: &toml_edit::Item) -> CargoResult<&str> {
637 if let Some(req) = old_dep.as_str() {
638 Ok(req)
639 } else if old_dep.is_table_like() {
640 let version = old_dep
641 .get("version")
642 .ok_or_else(|| anyhow::format_err!("Missing version field"))?;
643 version
644 .as_str()
645 .ok_or_else(|| anyhow::format_err!("Expect version to be a string"))
646 } else {
647 unreachable!("Invalid old dependency type")
648 }
649}
650
651fn old_version_compatible(dependency: &Dependency, old_version: &str) -> CargoResult<bool> {
652 let old_version = VersionReq::parse(old_version)
653 .with_context(|| parse_version_err(&dependency.name, old_version))?;
654
655 let current_version = match dependency.version() {
656 Some(current_version) => current_version,
657 None => return Ok(false),
658 };
659
660 let current_version = Version::parse(current_version)
661 .with_context(|| parse_version_err(&dependency.name, current_version))?;
662
663 Ok(old_version.matches(¤t_version))
664}
665
666pub fn str_or_1_len_table(item: &toml_edit::Item) -> bool {
667 item.is_str() || item.as_table_like().map(|t| t.len() == 1).unwrap_or(false)
668}
669
670fn print_upgrade_if_necessary(
672 crate_name: &str,
673 old_dep: &toml_edit::Item,
674 new_dep: &toml_edit::Item,
675) -> CargoResult<()> {
676 let old_version = get_version(old_dep)?;
677 let new_version = get_version(new_dep)?;
678
679 if old_version == new_version {
680 return Ok(());
681 }
682
683 let colorchoice = super::colorize_stderr();
684 let bufwtr = BufferWriter::stderr(colorchoice);
685 let mut buffer = bufwtr.buffer();
686 buffer
687 .set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))
688 .with_context(|| "Failed to set output colour")?;
689 write!(&mut buffer, " Upgrading ").with_context(|| "Failed to write upgrade message")?;
690 buffer
691 .set_color(&ColorSpec::new())
692 .with_context(|| "Failed to clear output colour")?;
693 writeln!(
694 &mut buffer,
695 "{} v{} -> v{}",
696 crate_name, old_version, new_version,
697 )
698 .with_context(|| "Failed to write upgrade versions")?;
699 bufwtr
700 .print(&buffer)
701 .with_context(|| "Failed to print upgrade message")?;
702
703 Ok(())
704}
705
706#[cfg(test)]
707mod tests {
708 use super::super::dependency::Dependency;
709 use super::*;
710
711 #[test]
712 fn add_remove_dependency() {
713 let root = dunce::canonicalize(Path::new("/")).expect("root exists");
714 let mut manifest = LocalManifest {
715 path: root.join("Cargo.toml"),
716 manifest: Manifest {
717 data: toml_edit::Document::new(),
718 },
719 };
720 let clone = manifest.clone();
721 let dep = Dependency::new("cargo-edit").set_version("0.1.0");
722 let _ = manifest.insert_into_table(&["dependencies".to_owned()], &dep);
723 assert!(manifest
724 .remove_from_table("dependencies", &dep.name)
725 .is_ok());
726 assert_eq!(manifest.data.to_string(), clone.data.to_string());
727 }
728
729 #[test]
730 fn update_dependency() {
731 let root = dunce::canonicalize(Path::new("/")).expect("root exists");
732 let mut manifest = LocalManifest {
733 path: root.join("Cargo.toml"),
734 manifest: Manifest {
735 data: toml_edit::Document::new(),
736 },
737 };
738 let dep = Dependency::new("cargo-edit").set_version("0.1.0");
739 manifest
740 .insert_into_table(&["dependencies".to_owned()], &dep)
741 .unwrap();
742
743 let new_dep = Dependency::new("cargo-edit").set_version("0.2.0");
744 manifest
745 .update_table_entry(&["dependencies".to_owned()], &new_dep, false)
746 .unwrap();
747 }
748
749 #[test]
750 fn update_wrong_dependency() {
751 let root = dunce::canonicalize(Path::new("/")).expect("root exists");
752 let mut manifest = LocalManifest {
753 path: root.join("Cargo.toml"),
754 manifest: Manifest {
755 data: toml_edit::Document::new(),
756 },
757 };
758 let dep = Dependency::new("cargo-edit").set_version("0.1.0");
759 manifest
760 .insert_into_table(&["dependencies".to_owned()], &dep)
761 .unwrap();
762 let original = manifest.clone();
763
764 let new_dep = Dependency::new("wrong-dep").set_version("0.2.0");
765 manifest
766 .update_table_entry(&["dependencies".to_owned()], &new_dep, false)
767 .unwrap();
768
769 assert_eq!(manifest.data.to_string(), original.data.to_string());
770 }
771
772 #[test]
773 fn remove_dependency_no_section() {
774 let root = dunce::canonicalize(Path::new("/")).expect("root exists");
775 let mut manifest = LocalManifest {
776 path: root.join("Cargo.toml"),
777 manifest: Manifest {
778 data: toml_edit::Document::new(),
779 },
780 };
781 let dep = Dependency::new("cargo-edit").set_version("0.1.0");
782 assert!(manifest
783 .remove_from_table("dependencies", &dep.name)
784 .is_err());
785 }
786
787 #[test]
788 fn remove_dependency_non_existent() {
789 let root = dunce::canonicalize(Path::new("/")).expect("root exists");
790 let mut manifest = LocalManifest {
791 path: root.join("Cargo.toml"),
792 manifest: Manifest {
793 data: toml_edit::Document::new(),
794 },
795 };
796 let dep = Dependency::new("cargo-edit").set_version("0.1.0");
797 let other_dep = Dependency::new("other-dep").set_version("0.1.0");
798 assert!(manifest
799 .insert_into_table(&["dependencies".to_owned()], &other_dep)
800 .is_ok());
801 assert!(manifest
802 .remove_from_table("dependencies", &dep.name)
803 .is_err());
804 }
805
806 #[test]
807 fn set_package_version_overrides() {
808 let original = r#"
809[package]
810name = "simple"
811version = "0.1.0"
812edition = "2015"
813
814[dependencies]
815"#;
816 let expected = r#"
817[package]
818name = "simple"
819version = "2.0.0"
820edition = "2015"
821
822[dependencies]
823"#;
824 let root = dunce::canonicalize(Path::new("/")).expect("root exists");
825 let mut manifest = LocalManifest {
826 path: root.join("Cargo.toml"),
827 manifest: original.parse::<Manifest>().unwrap(),
828 };
829 manifest.set_package_version(&semver::Version::parse("2.0.0").unwrap());
830 let actual = manifest.to_string();
831
832 assert_eq!(expected, actual);
833 }
834
835 #[test]
836 fn old_version_is_compatible() -> CargoResult<()> {
837 let with_version = Dependency::new("foo").set_version("2.3.4");
838 assert!(!old_version_compatible(&with_version, "1")?);
839 assert!(old_version_compatible(&with_version, "2")?);
840 assert!(!old_version_compatible(&with_version, "3")?);
841 Ok(())
842 }
843
844 #[test]
845 fn old_incompatible_with_missing_new_version() -> CargoResult<()> {
846 let no_version = Dependency::new("foo");
847 assert!(!old_version_compatible(&no_version, "1")?);
848 assert!(!old_version_compatible(&no_version, "2")?);
849 Ok(())
850 }
851
852 #[test]
853 fn old_incompatible_with_invalid() {
854 let bad_version = Dependency::new("foo").set_version("CAKE CAKE");
855 let good_version = Dependency::new("foo").set_version("1.2.3");
856 assert!(old_version_compatible(&bad_version, "1").is_err());
857 assert!(old_version_compatible(&good_version, "CAKE CAKE").is_err());
858 }
859}