cargo_edit_9/
manifest.rs

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/// A Cargo manifest
18#[derive(Debug, Clone)]
19pub struct Manifest {
20    /// Manifest contents as TOML data
21    pub data: toml_edit::Document,
22}
23
24impl Manifest {
25    /// Get the manifest's package name
26    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    /// Get the specified table from the manifest.
35    pub fn get_table<'a>(&'a self, table_path: &[String]) -> CargoResult<&'a toml_edit::Item> {
36        /// Descend into a manifest until the required table is found.
37        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    /// Get the specified table from the manifest.
60    pub fn get_table_mut<'a>(
61        &'a mut self,
62        table_path: &[String],
63    ) -> CargoResult<&'a mut toml_edit::Item> {
64        /// Descend into a manifest until the required table is found.
65        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    /// Get all sections in the manifest that exist and might contain dependencies.
86    /// The returned items are always `Table` or `InlineTable`.
87    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            // Dependencies can be in the three standard sections...
92            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            // ... and in `target.<target>.(build-/dev-)dependencies`.
105            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    /// returns features exposed by this manifest
133    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    /// Read manifest data from string
184    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/// A Cargo manifest that is available locally.
199#[derive(Debug)]
200pub struct LocalManifest {
201    /// Path to the manifest
202    pub path: PathBuf,
203    /// Manifest contents
204    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    /// Construct a `LocalManifest`. If no path is provided, make an educated guess as to which one
223    /// the user means.
224    pub fn find(path: Option<&Path>) -> CargoResult<Self> {
225        let path = dunce::canonicalize(find(path)?)?;
226        Self::try_new(&path)
227    }
228
229    /// Construct the `LocalManifest` corresponding to the `Path` provided.
230    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    /// Write changes back to the file
239    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    /// Instruct this manifest to upgrade a single dependency. If this manifest does not have that
265    /// dependency, it does nothing.
266    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    /// Lookup a dependency
295    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    /// Returns all dependencies
310    pub fn get_dependencies(
311        &self,
312    ) -> impl Iterator<Item = (Vec<String>, CargoResult<Dependency>)> + '_ {
313        self.filter_dependencies(|_| true)
314    }
315
316    /// Lookup a dependency
317    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    /// Add entry to a Cargo.toml.
367    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    /// Update an entry in Cargo.toml.
394    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    /// Update an entry with a specified name in Cargo.toml.
404    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 (and only if) there is an old entry, merge the new one in.
419        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    /// Remove entry from a Cargo.toml.
438    ///
439    /// # Examples
440    ///
441    /// ```
442    ///   use cargo_edit_9::{Dependency, LocalManifest, Manifest};
443    ///   use toml_edit;
444    ///
445    ///   let root = std::path::PathBuf::from("/").canonicalize().unwrap();
446    ///   let path = root.join("Cargo.toml");
447    ///   let mut manifest = LocalManifest { path, manifest: Manifest { data: toml_edit::Document::new() } };
448    ///   let dep = Dependency::new("cargo-edit").set_version("0.1.0");
449    ///   let _ = manifest.insert_into_table(&vec!["dependencies".to_owned()], &dep);
450    ///   assert!(manifest.remove_from_table("dependencies", &dep.name).is_ok());
451    ///   assert!(manifest.remove_from_table("dependencies", &dep.name).is_err());
452    ///   assert!(!manifest.data.contains_key("dependencies"));
453    /// ```
454    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            // remove the dependency
467            *dep = toml_edit::Item::None;
468        }
469
470        // remove table if empty
471        if parent_table.as_table_like().unwrap().is_empty() {
472            *parent_table = toml_edit::Item::None;
473        }
474
475        Ok(())
476    }
477
478    /// Add multiple dependencies to manifest
479    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    /// Allow mutating depedencies, wherever they live
488    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    /// Override the manifest's version
518    pub fn set_package_version(&mut self, version: &Version) {
519        self.data["package"]["version"] = toml_edit::value(version.to_string());
520    }
521
522    /// Remove references to `dep_key` if its no longer present
523    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    // Remove found idx in revers order so we don't invalidate the idx.
593    for idx in remove_list.iter().rev() {
594        feature_activations.remove(*idx);
595    }
596}
597
598/// If a manifest is specified, return that one, otherise perform a manifest search starting from
599/// the current directory.
600/// If a manifest is specified, return that one. If a path is specified, perform a manifest search
601/// starting from there. If nothing is specified, start searching from the current directory
602/// (`cwd`).
603pub 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
617/// Search for Cargo.toml in this directory and recursively up the tree until one is found.
618fn 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(&current_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
670/// Print a message if the new dependency version is different from the old one.
671fn 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}