Skip to main content

cuenv_release/
manifest.rs

1//! Cargo manifest reading and writing.
2//!
3//! This module provides functionality for reading and updating version
4//! information in Cargo.toml files, specifically handling the workspace
5//! version inheritance pattern.
6
7use crate::error::{Error, Result};
8use crate::version::Version;
9use std::collections::HashMap;
10use std::fs;
11use std::path::{Path, PathBuf};
12use toml_edit::{DocumentMut, Item, Value};
13
14/// Handles reading and writing Cargo.toml manifest files.
15pub struct CargoManifest {
16    root: PathBuf,
17}
18
19impl CargoManifest {
20    /// Create a new manifest handler for the given workspace root.
21    #[must_use]
22    pub fn new(root: &Path) -> Self {
23        Self {
24            root: root.to_path_buf(),
25        }
26    }
27
28    /// Get the path to the root Cargo.toml.
29    fn root_manifest_path(&self) -> PathBuf {
30        self.root.join("Cargo.toml")
31    }
32
33    /// Read and parse the root Cargo.toml.
34    fn read_root_manifest(&self) -> Result<DocumentMut> {
35        let path = self.root_manifest_path();
36        let content = fs::read_to_string(&path).map_err(|e| {
37            Error::manifest(
38                format!("Failed to read root Cargo.toml: {e}"),
39                Some(path.clone()),
40            )
41        })?;
42        content.parse::<DocumentMut>().map_err(|e| {
43            Error::manifest(format!("Failed to parse root Cargo.toml: {e}"), Some(path))
44        })
45    }
46
47    /// Read the workspace version from the root Cargo.toml.
48    ///
49    /// Looks for `[workspace.package].version`.
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if the file cannot be read or the version is not found.
54    pub fn read_workspace_version(&self) -> Result<Version> {
55        let doc = self.read_root_manifest()?;
56
57        let version_str = doc
58            .get("workspace")
59            .and_then(|w| w.get("package"))
60            .and_then(|p| p.get("version"))
61            .and_then(|v| v.as_str())
62            .ok_or_else(|| {
63                Error::manifest(
64                    "No [workspace.package].version found",
65                    Some(self.root_manifest_path()),
66                )
67            })?;
68
69        version_str.parse::<Version>()
70    }
71
72    /// Discover all workspace member directories.
73    fn discover_members(&self) -> Result<Vec<PathBuf>> {
74        let doc = self.read_root_manifest()?;
75
76        let members = doc
77            .get("workspace")
78            .and_then(|w| w.get("members"))
79            .and_then(|m| m.as_array())
80            .ok_or_else(|| {
81                Error::manifest(
82                    "No [workspace].members found",
83                    Some(self.root_manifest_path()),
84                )
85            })?;
86
87        let mut paths = Vec::new();
88        for member in members {
89            if let Some(pattern) = member.as_str() {
90                // Handle glob patterns
91                if pattern.contains('*') {
92                    let full_pattern = self.root.join(pattern);
93                    let pattern_str = full_pattern.to_str().ok_or_else(|| {
94                        Error::manifest(
95                            format!(
96                                "Workspace member glob pattern contains invalid UTF-8: {}",
97                                full_pattern.display()
98                            ),
99                            Some(full_pattern.clone()),
100                        )
101                    })?;
102                    let matches = glob::glob(pattern_str).map_err(|e| {
103                        Error::manifest(
104                            format!("Invalid glob pattern: {e}"),
105                            Some(full_pattern.clone()),
106                        )
107                    })?;
108                    for entry in matches.flatten() {
109                        if entry.is_dir() {
110                            paths.push(entry);
111                        }
112                    }
113                } else {
114                    paths.push(self.root.join(pattern));
115                }
116            }
117        }
118
119        Ok(paths)
120    }
121
122    /// Get all package names with their paths in the workspace.
123    ///
124    /// Returns a map of package names to their root directory paths.
125    ///
126    /// # Errors
127    ///
128    /// Returns an error if workspace members cannot be discovered or parsed.
129    pub fn get_package_paths(&self) -> Result<HashMap<String, PathBuf>> {
130        let members = self.discover_members()?;
131        let mut paths_map = HashMap::new();
132
133        for member_path in members {
134            let manifest_path = member_path.join("Cargo.toml");
135            if !manifest_path.exists() {
136                continue;
137            }
138
139            let content = fs::read_to_string(&manifest_path).map_err(|e| {
140                Error::manifest(
141                    format!("Failed to read {}: {e}", manifest_path.display()),
142                    Some(manifest_path.clone()),
143                )
144            })?;
145
146            let doc: toml::Value = toml::from_str(&content).map_err(|e| {
147                Error::manifest(
148                    format!("Failed to parse {}: {e}", manifest_path.display()),
149                    Some(manifest_path.clone()),
150                )
151            })?;
152
153            if let Some(name) = doc
154                .get("package")
155                .and_then(|p| p.get("name"))
156                .and_then(|n| n.as_str())
157            {
158                paths_map.insert(name.to_string(), member_path);
159            }
160        }
161
162        Ok(paths_map)
163    }
164
165    /// Get all package names in the workspace.
166    ///
167    /// # Errors
168    ///
169    /// Returns an error if workspace members cannot be discovered or parsed.
170    pub fn get_package_names(&self) -> Result<Vec<String>> {
171        let members = self.discover_members()?;
172        let mut names = Vec::new();
173
174        for member_path in members {
175            let manifest_path = member_path.join("Cargo.toml");
176            if !manifest_path.exists() {
177                continue;
178            }
179
180            let content = fs::read_to_string(&manifest_path).map_err(|e| {
181                Error::manifest(
182                    format!("Failed to read {}: {e}", manifest_path.display()),
183                    Some(manifest_path.clone()),
184                )
185            })?;
186
187            let doc: toml::Value = toml::from_str(&content).map_err(|e| {
188                Error::manifest(
189                    format!("Failed to parse {}: {e}", manifest_path.display()),
190                    Some(manifest_path.clone()),
191                )
192            })?;
193
194            if let Some(name) = doc
195                .get("package")
196                .and_then(|p| p.get("name"))
197                .and_then(|n| n.as_str())
198            {
199                names.push(name.to_string());
200            }
201        }
202
203        Ok(names)
204    }
205
206    /// Read all package versions, resolving workspace inheritance.
207    ///
208    /// For packages with `version.workspace = true`, the workspace version is used.
209    ///
210    /// # Errors
211    ///
212    /// Returns an error if package manifests cannot be read or parsed.
213    pub fn read_package_versions(&self) -> Result<HashMap<String, Version>> {
214        let workspace_version = self.read_workspace_version()?;
215        let members = self.discover_members()?;
216        let mut versions = HashMap::new();
217
218        for member_path in members {
219            let manifest_path = member_path.join("Cargo.toml");
220            if !manifest_path.exists() {
221                continue;
222            }
223
224            let content = fs::read_to_string(&manifest_path).map_err(|e| {
225                Error::manifest(
226                    format!("Failed to read {}: {e}", manifest_path.display()),
227                    Some(manifest_path.clone()),
228                )
229            })?;
230
231            let doc: toml::Value = toml::from_str(&content).map_err(|e| {
232                Error::manifest(
233                    format!("Failed to parse {}: {e}", manifest_path.display()),
234                    Some(manifest_path.clone()),
235                )
236            })?;
237
238            let Some(package) = doc.get("package") else {
239                continue;
240            };
241
242            let Some(name) = package.get("name").and_then(|n| n.as_str()) else {
243                continue;
244            };
245
246            // Check if version uses workspace inheritance
247            let version = if let Some(version_table) =
248                package.get("version").and_then(|v| v.as_table())
249                && version_table
250                    .get("workspace")
251                    .is_some_and(|w| w.as_bool() == Some(true))
252            {
253                workspace_version.clone()
254            } else if package.get("version").and_then(|v| v.as_table()).is_some() {
255                // Edge case: 'version' is a table, but not a workspace inheritance table (i.e., not { workspace = true }).
256                // This is not a valid Cargo manifest configuration; skip this package.
257                continue;
258            } else if let Some(version_str) = package.get("version").and_then(|v| v.as_str()) {
259                version_str.parse::<Version>()?
260            } else {
261                continue;
262            };
263
264            versions.insert(name.to_string(), version);
265        }
266
267        Ok(versions)
268    }
269
270    /// Update the workspace version in the root Cargo.toml.
271    ///
272    /// This updates `[workspace.package].version`.
273    ///
274    /// # Errors
275    ///
276    /// Returns an error if the file cannot be read, parsed, or written.
277    pub fn update_workspace_version(&self, new_version: &Version) -> Result<()> {
278        let path = self.root_manifest_path();
279        let content = fs::read_to_string(&path).map_err(|e| {
280            Error::manifest(
281                format!("Failed to read root Cargo.toml: {e}"),
282                Some(path.clone()),
283            )
284        })?;
285
286        let mut doc = content.parse::<DocumentMut>().map_err(|e| {
287            Error::manifest(
288                format!("Failed to parse root Cargo.toml: {e}"),
289                Some(path.clone()),
290            )
291        })?;
292
293        // Update [workspace.package].version
294        if let Some(workspace) = doc.get_mut("workspace")
295            && let Some(package) = workspace.get_mut("package")
296            && let Item::Table(pkg_table) = package
297        {
298            pkg_table["version"] = toml_edit::value(new_version.to_string());
299        } else {
300            return Err(Error::manifest(
301                "Root manifest is missing [workspace.package] table".to_string(),
302                Some(path),
303            ));
304        }
305
306        fs::write(&path, doc.to_string()).map_err(|e| {
307            Error::manifest(format!("Failed to write root Cargo.toml: {e}"), Some(path))
308        })?;
309
310        Ok(())
311    }
312
313    /// Read package dependencies from member manifests.
314    ///
315    /// Returns a map of package names to their workspace-internal dependencies.
316    /// Only includes dependencies that are defined in `[workspace.dependencies]`
317    /// with a `path` key, indicating they are internal to the workspace.
318    ///
319    /// # Errors
320    ///
321    /// Returns an error if package manifests cannot be read or parsed.
322    pub fn read_package_dependencies(&self) -> Result<HashMap<String, Vec<String>>> {
323        // Step 1: Read workspace.dependencies to find internal packages
324        // Internal packages are those defined with `path = ...` in workspace.dependencies
325        let internal_packages = self.get_internal_package_names()?;
326
327        // Step 2: For each member, collect only internal workspace dependencies
328        let members = self.discover_members()?;
329        let mut dependencies_map = HashMap::new();
330
331        for member_path in members {
332            let manifest_path = member_path.join("Cargo.toml");
333            if !manifest_path.exists() {
334                continue;
335            }
336
337            let content = fs::read_to_string(&manifest_path).map_err(|e| {
338                Error::manifest(
339                    format!("Failed to read {}: {e}", manifest_path.display()),
340                    Some(manifest_path.clone()),
341                )
342            })?;
343
344            let doc: toml::Value = toml::from_str(&content).map_err(|e| {
345                Error::manifest(
346                    format!("Failed to parse {}: {e}", manifest_path.display()),
347                    Some(manifest_path.clone()),
348                )
349            })?;
350
351            let Some(package) = doc.get("package") else {
352                continue;
353            };
354
355            let Some(name) = package.get("name").and_then(|n| n.as_str()) else {
356                continue;
357            };
358
359            // Collect workspace-internal dependencies
360            let mut deps = Vec::new();
361
362            // Check [dependencies]
363            if let Some(dependencies) = doc.get("dependencies").and_then(|d| d.as_table()) {
364                for (dep_name, dep_value) in dependencies {
365                    // Check if it's a workspace dependency that references an internal package
366                    if let Some(dep_table) = dep_value.as_table() {
367                        let is_workspace =
368                            dep_table.get("workspace") == Some(&toml::Value::Boolean(true));
369                        let has_path = dep_table.contains_key("path");
370                        let is_internal = internal_packages.contains(dep_name);
371
372                        // Include if: has explicit path OR (is workspace dep AND is internal package)
373                        if has_path || (is_workspace && is_internal) {
374                            deps.push(dep_name.clone());
375                        }
376                    }
377                }
378            }
379
380            dependencies_map.insert(name.to_string(), deps);
381        }
382
383        Ok(dependencies_map)
384    }
385
386    /// Get the names of internal packages from `[workspace.dependencies]`.
387    ///
388    /// Internal packages are those defined with a `path` key.
389    fn get_internal_package_names(&self) -> Result<std::collections::HashSet<String>> {
390        use std::collections::HashSet;
391
392        let doc = self.read_root_manifest()?;
393        let mut internal_packages = HashSet::new();
394
395        if let Some(workspace) = doc.get("workspace")
396            && let Some(deps) = workspace.get("dependencies")
397            && let Some(deps_table) = deps.as_table()
398        {
399            for (name, value) in deps_table {
400                // Check for inline table: { path = "...", version = "..." }
401                if let Some(inline) = value.as_inline_table()
402                    && inline.contains_key("path")
403                {
404                    internal_packages.insert(name.to_string());
405                }
406                // Check for regular table (dotted keys)
407                if let Some(table) = value.as_table()
408                    && table.contains_key("path")
409                {
410                    internal_packages.insert(name.to_string());
411                }
412            }
413        }
414
415        Ok(internal_packages)
416    }
417
418    /// Update workspace dependency versions in the root Cargo.toml.
419    ///
420    /// This updates versions in `[workspace.dependencies]` for internal crates.
421    ///
422    /// # Errors
423    ///
424    /// Returns an error if the file cannot be read, parsed, or written.
425    pub fn update_workspace_dependency_versions(
426        &self,
427        packages: &HashMap<String, Version>,
428    ) -> Result<()> {
429        let path = self.root_manifest_path();
430        let content = fs::read_to_string(&path).map_err(|e| {
431            Error::manifest(
432                format!("Failed to read root Cargo.toml: {e}"),
433                Some(path.clone()),
434            )
435        })?;
436
437        let mut doc = content.parse::<DocumentMut>().map_err(|e| {
438            Error::manifest(
439                format!("Failed to parse root Cargo.toml: {e}"),
440                Some(path.clone()),
441            )
442        })?;
443
444        // Update [workspace.dependencies]
445        if let Some(workspace) = doc.get_mut("workspace")
446            && let Some(deps) = workspace.get_mut("dependencies")
447            && let Item::Table(deps_table) = deps
448        {
449            for (pkg_name, version) in packages {
450                if let Some(dep) = deps_table.get_mut(pkg_name) {
451                    // Dependencies can be either inline tables or dotted keys
452                    match dep {
453                        Item::Value(Value::InlineTable(table)) => {
454                            table.insert("version", new_version_value(version));
455                        }
456                        Item::Table(table) => {
457                            table["version"] = toml_edit::value(version.to_string());
458                        }
459                        _ => {}
460                    }
461                }
462            }
463        }
464        // Note: If [workspace.dependencies] doesn't exist, that's OK - not all workspaces use it
465
466        fs::write(&path, doc.to_string()).map_err(|e| {
467            Error::manifest(format!("Failed to write root Cargo.toml: {e}"), Some(path))
468        })?;
469
470        Ok(())
471    }
472}
473
474/// Create a new TOML value for a version string.
475fn new_version_value(version: &Version) -> Value {
476    Value::from(version.to_string())
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482    use tempfile::TempDir;
483
484    fn create_test_workspace(temp: &TempDir) -> PathBuf {
485        let root = temp.path().to_path_buf();
486
487        // Create root Cargo.toml
488        let root_manifest = r#"[workspace]
489resolver = "2"
490members = ["crates/foo", "crates/bar"]
491
492[workspace.package]
493version = "1.2.3"
494edition = "2021"
495
496[workspace.dependencies]
497foo = { path = "crates/foo", version = "1.2.3" }
498bar = { path = "crates/bar", version = "1.2.3" }
499"#;
500        fs::write(root.join("Cargo.toml"), root_manifest).unwrap();
501
502        // Create member crates
503        fs::create_dir_all(root.join("crates/foo")).unwrap();
504        fs::create_dir_all(root.join("crates/bar")).unwrap();
505
506        let foo_manifest = r#"[package]
507name = "foo"
508version.workspace = true
509"#;
510        fs::write(root.join("crates/foo/Cargo.toml"), foo_manifest).unwrap();
511
512        let bar_manifest = r#"[package]
513name = "bar"
514version.workspace = true
515"#;
516        fs::write(root.join("crates/bar/Cargo.toml"), bar_manifest).unwrap();
517
518        root
519    }
520
521    // ==========================================================================
522    // CargoManifest construction tests
523    // ==========================================================================
524
525    #[test]
526    fn test_cargo_manifest_new() {
527        let temp = TempDir::new().unwrap();
528        let manifest = CargoManifest::new(temp.path());
529        // Just verify construction works
530        let _ = manifest;
531    }
532
533    // ==========================================================================
534    // read_workspace_version tests
535    // ==========================================================================
536
537    #[test]
538    fn test_read_workspace_version() {
539        let temp = TempDir::new().unwrap();
540        let root = create_test_workspace(&temp);
541
542        let manifest = CargoManifest::new(&root);
543        let version = manifest.read_workspace_version().unwrap();
544
545        assert_eq!(version, Version::new(1, 2, 3));
546    }
547
548    #[test]
549    fn test_read_workspace_version_no_file() {
550        let temp = TempDir::new().unwrap();
551        let manifest = CargoManifest::new(temp.path());
552
553        let result = manifest.read_workspace_version();
554        assert!(result.is_err());
555    }
556
557    #[test]
558    fn test_read_workspace_version_no_workspace_package() {
559        let temp = TempDir::new().unwrap();
560        let root = temp.path().to_path_buf();
561
562        // Create a minimal Cargo.toml without workspace.package.version
563        let manifest_content = r#"[workspace]
564members = ["crates/foo"]
565"#;
566        fs::write(root.join("Cargo.toml"), manifest_content).unwrap();
567
568        let manifest = CargoManifest::new(&root);
569        let result = manifest.read_workspace_version();
570        assert!(result.is_err());
571    }
572
573    // ==========================================================================
574    // get_package_names tests
575    // ==========================================================================
576
577    #[test]
578    fn test_get_package_names() {
579        let temp = TempDir::new().unwrap();
580        let root = create_test_workspace(&temp);
581
582        let manifest = CargoManifest::new(&root);
583        let mut names = manifest.get_package_names().unwrap();
584        names.sort();
585
586        assert_eq!(names, vec!["bar", "foo"]);
587    }
588
589    #[test]
590    fn test_get_package_names_no_members() {
591        let temp = TempDir::new().unwrap();
592        let root = temp.path().to_path_buf();
593
594        // No [workspace].members
595        let manifest_content = r#"[workspace]
596resolver = "2"
597
598[workspace.package]
599version = "1.0.0"
600"#;
601        fs::write(root.join("Cargo.toml"), manifest_content).unwrap();
602
603        let manifest = CargoManifest::new(&root);
604        let result = manifest.get_package_names();
605        assert!(result.is_err());
606    }
607
608    #[test]
609    fn test_get_package_names_missing_member_cargo_toml() {
610        let temp = TempDir::new().unwrap();
611        let root = temp.path().to_path_buf();
612
613        // Create workspace that references non-existent member
614        let manifest_content = r#"[workspace]
615resolver = "2"
616members = ["crates/nonexistent"]
617
618[workspace.package]
619version = "1.0.0"
620"#;
621        fs::write(root.join("Cargo.toml"), manifest_content).unwrap();
622        fs::create_dir_all(root.join("crates/nonexistent")).unwrap();
623        // Don't create the member's Cargo.toml
624
625        let manifest = CargoManifest::new(&root);
626        let names = manifest.get_package_names().unwrap();
627        // Should return empty since the member has no Cargo.toml
628        assert!(names.is_empty());
629    }
630
631    // ==========================================================================
632    // get_package_paths tests
633    // ==========================================================================
634
635    #[test]
636    fn test_get_package_paths() {
637        let temp = TempDir::new().unwrap();
638        let root = create_test_workspace(&temp);
639
640        let manifest = CargoManifest::new(&root);
641        let paths = manifest.get_package_paths().unwrap();
642
643        assert!(paths.contains_key("foo"));
644        assert!(paths.contains_key("bar"));
645        assert!(paths.get("foo").unwrap().ends_with("crates/foo"));
646    }
647
648    // ==========================================================================
649    // read_package_versions tests
650    // ==========================================================================
651
652    #[test]
653    fn test_read_package_versions() {
654        let temp = TempDir::new().unwrap();
655        let root = create_test_workspace(&temp);
656
657        let manifest = CargoManifest::new(&root);
658        let versions = manifest.read_package_versions().unwrap();
659
660        assert_eq!(versions.get("foo"), Some(&Version::new(1, 2, 3)));
661        assert_eq!(versions.get("bar"), Some(&Version::new(1, 2, 3)));
662    }
663
664    #[test]
665    fn test_read_package_versions_explicit_version() {
666        let temp = TempDir::new().unwrap();
667        let root = temp.path().to_path_buf();
668
669        // Create workspace
670        let root_manifest = r#"[workspace]
671resolver = "2"
672members = ["crates/explicit"]
673
674[workspace.package]
675version = "1.0.0"
676"#;
677        fs::write(root.join("Cargo.toml"), root_manifest).unwrap();
678
679        // Create member with explicit version
680        fs::create_dir_all(root.join("crates/explicit")).unwrap();
681        let member_manifest = r#"[package]
682name = "explicit"
683version = "2.0.0"
684"#;
685        fs::write(root.join("crates/explicit/Cargo.toml"), member_manifest).unwrap();
686
687        let manifest = CargoManifest::new(&root);
688        let versions = manifest.read_package_versions().unwrap();
689
690        assert_eq!(versions.get("explicit"), Some(&Version::new(2, 0, 0)));
691    }
692
693    // ==========================================================================
694    // update_workspace_version tests
695    // ==========================================================================
696
697    #[test]
698    fn test_update_workspace_version() {
699        let temp = TempDir::new().unwrap();
700        let root = create_test_workspace(&temp);
701
702        let manifest = CargoManifest::new(&root);
703        manifest
704            .update_workspace_version(&Version::new(2, 0, 0))
705            .unwrap();
706
707        // Read back and verify
708        let new_version = manifest.read_workspace_version().unwrap();
709        assert_eq!(new_version, Version::new(2, 0, 0));
710    }
711
712    #[test]
713    fn test_update_workspace_version_no_workspace_package() {
714        let temp = TempDir::new().unwrap();
715        let root = temp.path().to_path_buf();
716
717        // Create a minimal Cargo.toml without [workspace.package]
718        let manifest_content = r#"[workspace]
719members = ["crates/foo"]
720"#;
721        fs::write(root.join("Cargo.toml"), manifest_content).unwrap();
722
723        let manifest = CargoManifest::new(&root);
724        let result = manifest.update_workspace_version(&Version::new(2, 0, 0));
725        assert!(result.is_err());
726    }
727
728    // ==========================================================================
729    // update_workspace_dependency_versions tests
730    // ==========================================================================
731
732    #[test]
733    fn test_update_workspace_dependency_versions() {
734        let temp = TempDir::new().unwrap();
735        let root = create_test_workspace(&temp);
736
737        let manifest = CargoManifest::new(&root);
738        let packages = HashMap::from([
739            ("foo".to_string(), Version::new(2, 0, 0)),
740            ("bar".to_string(), Version::new(2, 0, 0)),
741        ]);
742        manifest
743            .update_workspace_dependency_versions(&packages)
744            .unwrap();
745
746        // Read back and verify the content was updated
747        let content = fs::read_to_string(root.join("Cargo.toml")).unwrap();
748        assert!(content.contains("version = \"2.0.0\""));
749    }
750
751    #[test]
752    fn test_update_workspace_dependency_versions_no_deps() {
753        let temp = TempDir::new().unwrap();
754        let root = temp.path().to_path_buf();
755
756        // Create workspace without [workspace.dependencies]
757        let manifest_content = r#"[workspace]
758resolver = "2"
759members = []
760
761[workspace.package]
762version = "1.0.0"
763"#;
764        fs::write(root.join("Cargo.toml"), manifest_content).unwrap();
765
766        let manifest = CargoManifest::new(&root);
767        let packages = HashMap::from([("foo".to_string(), Version::new(2, 0, 0))]);
768
769        // Should succeed silently - no workspace.dependencies to update
770        manifest
771            .update_workspace_dependency_versions(&packages)
772            .unwrap();
773    }
774
775    #[test]
776    fn test_update_workspace_dependency_versions_partial_update() {
777        let temp = TempDir::new().unwrap();
778        let root = create_test_workspace(&temp);
779
780        let manifest = CargoManifest::new(&root);
781        // Only update foo, not bar
782        let packages = HashMap::from([("foo".to_string(), Version::new(3, 0, 0))]);
783        manifest
784            .update_workspace_dependency_versions(&packages)
785            .unwrap();
786
787        // Read back and verify foo was updated
788        let content = fs::read_to_string(root.join("Cargo.toml")).unwrap();
789        assert!(content.contains("version = \"3.0.0\""));
790        // bar should still have the old version
791        assert!(content.contains("version = \"1.2.3\""));
792    }
793
794    // ==========================================================================
795    // read_package_dependencies tests
796    // ==========================================================================
797
798    #[test]
799    fn test_read_package_dependencies() {
800        let temp = TempDir::new().unwrap();
801        let root = temp.path().to_path_buf();
802
803        // Create workspace with dependencies
804        let root_manifest = r#"[workspace]
805resolver = "2"
806members = ["crates/app", "crates/lib"]
807
808[workspace.package]
809version = "1.0.0"
810
811[workspace.dependencies]
812lib = { path = "crates/lib", version = "1.0.0" }
813"#;
814        fs::write(root.join("Cargo.toml"), root_manifest).unwrap();
815
816        // Create lib (no deps)
817        fs::create_dir_all(root.join("crates/lib")).unwrap();
818        let lib_manifest = r#"[package]
819name = "lib"
820version.workspace = true
821"#;
822        fs::write(root.join("crates/lib/Cargo.toml"), lib_manifest).unwrap();
823
824        // Create app (depends on lib)
825        fs::create_dir_all(root.join("crates/app")).unwrap();
826        let app_manifest = r#"[package]
827name = "app"
828version.workspace = true
829
830[dependencies]
831lib = { workspace = true }
832"#;
833        fs::write(root.join("crates/app/Cargo.toml"), app_manifest).unwrap();
834
835        let manifest = CargoManifest::new(&root);
836        let deps = manifest.read_package_dependencies().unwrap();
837
838        assert!(deps.contains_key("app"));
839        assert!(deps.contains_key("lib"));
840        assert!(deps.get("app").unwrap().contains(&"lib".to_string()));
841        assert!(deps.get("lib").unwrap().is_empty());
842    }
843
844    // ==========================================================================
845    // discover_members with glob patterns tests
846    // ==========================================================================
847
848    #[test]
849    fn test_discover_members_glob_pattern() {
850        let temp = TempDir::new().unwrap();
851        let root = temp.path().to_path_buf();
852
853        // Create workspace with glob pattern
854        let root_manifest = r#"[workspace]
855resolver = "2"
856members = ["crates/*"]
857
858[workspace.package]
859version = "1.0.0"
860"#;
861        fs::write(root.join("Cargo.toml"), root_manifest).unwrap();
862
863        // Create member crates that match the glob
864        fs::create_dir_all(root.join("crates/alpha")).unwrap();
865        fs::create_dir_all(root.join("crates/beta")).unwrap();
866
867        let alpha_manifest = r#"[package]
868name = "alpha"
869version.workspace = true
870"#;
871        fs::write(root.join("crates/alpha/Cargo.toml"), alpha_manifest).unwrap();
872
873        let beta_manifest = r#"[package]
874name = "beta"
875version.workspace = true
876"#;
877        fs::write(root.join("crates/beta/Cargo.toml"), beta_manifest).unwrap();
878
879        let manifest = CargoManifest::new(&root);
880        let mut names = manifest.get_package_names().unwrap();
881        names.sort();
882
883        assert_eq!(names, vec!["alpha", "beta"]);
884    }
885
886    // ==========================================================================
887    // new_version_value helper tests
888    // ==========================================================================
889
890    #[test]
891    fn test_new_version_value() {
892        let version = Version::new(1, 2, 3);
893        let value = new_version_value(&version);
894        assert_eq!(value.as_str(), Some("1.2.3"));
895    }
896
897    #[test]
898    fn test_new_version_value_prerelease() {
899        let version = "1.0.0-alpha.1".parse::<Version>().unwrap();
900        let value = new_version_value(&version);
901        assert_eq!(value.as_str(), Some("1.0.0-alpha.1"));
902    }
903}