guppy-summaries-0.7.1/.cargo_vcs_info.json0000644000000001550000000000100142110ustar { "git": { "sha1": "77d94d02d6bda2d05b52906ebfc0ca9fee373544" }, "path_in_vcs": "guppy-summaries" }guppy-summaries-0.7.1/CHANGELOG.md000064400000000000000000000061111046102023000146100ustar 00000000000000# Changelog ## [0.7.1] - 2022-09-30 ### Changed - Repository location update. - MSRV updated to Rust 1.58. ## [0.7.0] - 2022-03-14 ### Added - Support for optional dependencies, as part of guppy's support for [namespaced features]: - `PackageInfo` has a new `optional_deps` field. - `SummaryDiffStatus::Modified` has new `added_optional_deps`, `removed_optional_deps` and `unchanged_optional_deps` fields. [namespaced features]: https://rust-lang.github.io/rfcs/3143-cargo-weak-namespaced-features.html ### Changed - MSRV updated to Rust 1.56. ## [0.6.1] - 2021-11-23 ### Changed - The `toml` crate is now built with the `preserve_order` feature. - This feature ensures that the key ordering in metadata is preserved. ## [0.6.0] - 2021-11-23 This is a minor breaking change that should not affect most consumers. ### Changed - `SummaryWithMetadata` is now simply `Summary`, and no longer takes a type parameter. - `metadata` is now a `toml::value::Table`. - `path_forward_slashes` is no longer exposed as a helper. ## [0.5.1] - 2021-10-01 ### Added - `SummaryId` now implements `Display`, printing out the ID as a TOML inline table. - A new convenience module `path_forward_slashes` is provided to serialize and deserialize paths using forward slashes. ## [0.5.0] - 2021-09-13 ### Changed - Public dependency version bumps: - `semver` updated to 1.0. - `diffus` updated to 0.10.0. ## [0.4.0] - 2021-02-23 ### Changed - `guppy-summaries` now uses [`camino`](https://crates.io/crates/camino) `Utf8Path` and `Utf8PathBuf` wrappers. These wrappers provide type-level assertions that returned paths are valid UTF-8. ## [0.3.2] - 2021-02-04 ### Fixed - `SummarySource` paths are now always output with forward slashes, including on Windows. ## [0.3.1] - 2020-12-09 ### Added - Support for serializing `SummaryDiff` instances (thanks @mimoo). ## [0.3.0] - 2020-12-02 ### Changed - Updated semver to 0.11. ## [0.2.0] - 2020-06-20 ### Changed - Move diff-related types into a new `diff` module. - Don't export `Summary` as a default type alias any more. - Remove `parse_with_metadata` in favor of making `parse` generic. ## [0.1.0] - 2020-06-12 Initial release. [0.7.1]: https://github.com/guppy-rs/guppy/releases/tag/guppy-summaries-0.7.1 [0.7.0]: https://github.com/guppy-rs/guppy/releases/tag/guppy-summaries-0.7.0 [0.6.1]: https://github.com/guppy-rs/guppy/releases/tag/guppy-summaries-0.6.1 [0.6.0]: https://github.com/guppy-rs/guppy/releases/tag/guppy-summaries-0.6.0 [0.5.1]: https://github.com/guppy-rs/guppy/releases/tag/guppy-summaries-0.5.1 [0.5.0]: https://github.com/guppy-rs/guppy/releases/tag/guppy-summaries-0.5.0 [0.4.0]: https://github.com/guppy-rs/guppy/releases/tag/guppy-summaries-0.4.0 [0.3.2]: https://github.com/guppy-rs/guppy/releases/tag/guppy-summaries-0.3.2 [0.3.1]: https://github.com/guppy-rs/guppy/releases/tag/guppy-summaries-0.3.1 [0.3.0]: https://github.com/guppy-rs/guppy/releases/tag/guppy-summaries-0.3.0 [0.2.0]: https://github.com/guppy-rs/guppy/releases/tag/guppy-summaries-0.2.0 [0.1.0]: https://github.com/guppy-rs/guppy/releases/tag/guppy-summaries-0.1.0 guppy-summaries-0.7.1/Cargo.toml0000644000000031100000000000100122010ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.58" name = "guppy-summaries" version = "0.7.1" authors = ["Rain "] exclude = ["README.tpl"] description = "Build summaries for Cargo, created by guppy." documentation = "https://docs.rs/guppy-summaries" readme = "README.md" keywords = [ "cargo", "dependencies", "guppy", "summaries", ] categories = [ "config", "data-structures", "development-tools", "parser-implementations", ] license = "MIT OR Apache-2.0" repository = "https://github.com/guppy-rs/guppy" [package.metadata.docs.rs] all-features = true [dependencies.camino] version = "1.0.9" features = ["serde1"] [dependencies.cfg-if] version = "1.0.0" [dependencies.diffus] version = "0.10.0" [dependencies.guppy-workspace-hack] version = "0.1" [dependencies.semver] version = "1.0.9" features = ["serde"] [dependencies.serde] version = "1.0.137" features = ["derive"] [dependencies.toml] version = "0.5.9" features = ["preserve_order"] [dev-dependencies.indoc] version = "1.0.6" [dev-dependencies.pretty_assertions] version = "1.2.1" [dev-dependencies.serde_json] version = "1.0.81" guppy-summaries-0.7.1/Cargo.toml.orig000064400000000000000000000020571046102023000156730ustar 00000000000000[package] name = "guppy-summaries" version = "0.7.1" description = "Build summaries for Cargo, created by guppy." documentation = "https://docs.rs/guppy-summaries" repository = "https://github.com/guppy-rs/guppy" authors = ["Rain "] license = "MIT OR Apache-2.0" readme = "README.md" keywords = ["cargo", "dependencies", "guppy", "summaries"] categories = [ "config", "data-structures", "development-tools", "parser-implementations", ] edition = "2021" exclude = [ # Readme template that doesn't need to be included. "README.tpl", ] rust-version = "1.58" [package.metadata.docs.rs] all-features = true [dependencies] camino = { version = "1.0.9", features = ["serde1"] } cfg-if = "1.0.0" diffus = "0.10.0" toml = { version = "0.5.9", features = ["preserve_order"] } semver = { version = "1.0.9", features = ["serde"] } serde = { version = "1.0.137", features = ["derive"] } guppy-workspace-hack = { version = "0.1", path = "../workspace-hack" } [dev-dependencies] indoc = "1.0.6" pretty_assertions = "1.2.1" serde_json = "1.0.81" guppy-summaries-0.7.1/README.md000064400000000000000000000072651046102023000142710ustar 00000000000000# guppy-summaries [![guppy-summaries on crates.io](https://img.shields.io/crates/v/guppy-summaries)](https://crates.io/crates/guppy-summaries) [![Documentation (latest release)](https://docs.rs/guppy-summaries/badge.svg)](https://docs.rs/guppy-summaries/) [![Documentation (main)](https://img.shields.io/badge/docs-main-brightgreen)](https://guppy-rs.github.io/guppy/rustdoc/guppy_summaries/) [![License](https://img.shields.io/badge/license-Apache-green.svg)](../LICENSE-APACHE) [![License](https://img.shields.io/badge/license-MIT-green.svg)](../LICENSE-MIT) Facilities to serialize, deserialize and compare build summaries. A *build summary* is a record of what packages and features are built on the target and host platforms. A summary file can be checked into a repository, kept up to date and compared in CI, and allow for tracking results of builds over time. `guppy-summaries` is designed to be small and independent of the main `guppy` crate. ## Examples ```rust use guppy_summaries::{Summary, SummaryId, SummarySource, PackageStatus}; use pretty_assertions::assert_eq; use semver::Version; use std::collections::BTreeSet; use toml::Value; // A summary is a TOML file that has this format: static SUMMARY: &str = r#" [[target-package]] name = "foo" version = "1.2.3" workspace-path = "foo" status = 'initial' features = ["feature-a", "feature-c"] [[host-package]] name = "proc-macro" version = "0.1.2" workspace-path = "proc-macros/macro" status = 'workspace' features = ["macro-expand"] [[host-package]] name = "bar" version = "0.4.5" crates-io = true status = 'direct' features = [] "#; // The summary can be deserialized: let summary = Summary::parse(SUMMARY).expect("from_str succeeded"); // ... and a package and its features can be looked up. let summary_id = SummaryId::new("foo", Version::new(1, 2, 3), SummarySource::workspace("foo")); let info = &summary.target_packages[&summary_id]; assert_eq!(info.status, PackageStatus::Initial, "correct status"); assert_eq!( info.features.iter().map(|feature| feature.as_str()).collect::>(), ["feature-a", "feature-c"], "correct feature list" ); // Another summary. static SUMMARY2: &str = r#" [[target-package]] name = "foo" version = "1.2.4" workspace-path = "new-location/foo" status = 'initial' features = ["feature-a", "feature-b"] [[target-package]] name = "once_cell" version = "1.4.0" source = "git+https://github.com/matklad/once_cell?tag=v1.4.0" status = 'transitive' features = ["std"] [[host-package]] name = "bar" version = "0.4.5" crates-io = true status = 'direct' features = [] "#; let summary2 = Summary::parse(SUMMARY2).expect("from_str succeeded"); // Diff summary and summary2. let diff = summary.diff(&summary2); // Pretty-print a report generated from the diff. let diff_str = format!("{}", diff.report()); assert_eq!( r#"target packages: A once_cell 1.4.0 (transitive third-party, external 'git+https://github.com/matklad/once_cell?tag=v1.4.0') * features: std M foo 1.2.4 (initial, path 'new-location/foo') * version upgraded from 1.2.3 * source changed from path 'foo' * added features: feature-b * removed features: feature-c * (unchanged features: feature-a) * (unchanged optional dependencies: [none]) host packages: R proc-macro 0.1.2 (workspace, path 'proc-macros/macro') * (old features: macro-expand) "#, diff_str, ); ``` ## Contributing See the [CONTRIBUTING](../CONTRIBUTING.md) file for how to help out. ## License This project is available under the terms of either the [Apache 2.0 license](../LICENSE-APACHE) or the [MIT license](../LICENSE-MIT). guppy-summaries-0.7.1/src/diff.rs000064400000000000000000000446121046102023000150540ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Compare and diff summaries. //! //! A diff of two summaries is a list of changes between them. //! //! The main entry point is `SummaryDiff`, which can be created through the `diff` method on //! summaries or through `SummaryDiff::new`. pub use crate::report::SummaryReport; use crate::{PackageInfo, PackageMap, PackageStatus, Summary, SummaryId, SummarySource}; use diffus::{edit, Diffable}; use semver::Version; use serde::{ser::SerializeStruct, Serialize}; use std::{ collections::{BTreeMap, BTreeSet, HashMap}, fmt, mem, }; /// A diff of two package summaries. /// /// This struct contains information on the packages that were changed, as well as those that were /// not. /// /// ## Human-readable reports /// /// The [`report`](SummaryDiff::report) method can be used with `fmt::Display` to generate a /// friendly, human-readable report. /// /// ## Machine-readable serialization /// /// A `SummaryDiff` can be serialized through `serde`. The output format is part of the API. /// /// An example of TOML-serialized output: /// /// ```toml /// [[target-packages.changed]] /// name = "dep" /// version = "0.4.3" /// crates-io = true /// change = "added" /// status = "direct" /// features = ["std"] /// /// [[target-packages.changed]] /// name = "foo" /// version = "1.2.3" /// workspace-path = "foo" /// change = "modified" /// new-status = "initial" /// added-features = ["feature2"] /// removed-features = [] /// unchanged-features = ["default", "feature1"] /// /// [[target-packages.unchanged]] /// name = "no-changes" /// version = "1.5.3" /// crates-io = true /// status = "transitive" /// features = ["default"] /// /// [[host-packages.changed]] /// name = "dep" /// version = "0.4.2" /// crates-io = true /// change = "removed" /// old-status = "direct" /// old-features = ["std"] /// ``` #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "kebab-case")] pub struct SummaryDiff<'a> { /// Diff of target packages. pub target_packages: PackageDiff<'a>, /// Diff of host packages. pub host_packages: PackageDiff<'a>, } impl<'a> SummaryDiff<'a> { /// Computes a diff between two summaries. pub fn new(old: &'a Summary, new: &'a Summary) -> Self { Self { target_packages: PackageDiff::new(&old.target_packages, &new.target_packages), host_packages: PackageDiff::new(&old.host_packages, &new.host_packages), } } /// Returns true if there are any changes in this diff. pub fn is_changed(&self) -> bool { !self.is_unchanged() } /// Returns true if there are no changes in this diff. pub fn is_unchanged(&self) -> bool { self.target_packages.is_unchanged() && self.host_packages.is_unchanged() } /// Returns a report for this diff. /// /// This report can be used with `fmt::Display`. pub fn report<'b>(&'b self) -> SummaryReport<'a, 'b> { SummaryReport::new(self) } } /// Type alias for list entries in the `PackageDiff::unchanged` map. pub type UnchangedInfo<'a> = (&'a Version, &'a SummarySource, &'a PackageInfo); /// A diff from a particular section of a summary. #[derive(Clone, Debug, Eq, PartialEq)] pub struct PackageDiff<'a> { /// Changed packages. pub changed: BTreeMap<&'a SummaryId, SummaryDiffStatus<'a>>, /// Unchanged packages, keyed by name. pub unchanged: BTreeMap<&'a str, Vec>>, } impl<'a> PackageDiff<'a> { /// Constructs a new `PackageDiff` from a pair of `PackageMap` instances. pub fn new(old: &'a PackageMap, new: &'a PackageMap) -> Self { let mut changed = BTreeMap::new(); let mut unchanged = BTreeMap::new(); let mut add_unchanged = |summary_id: &'a SummaryId, info: &'a PackageInfo| { unchanged .entry(summary_id.name.as_str()) .or_insert_with(Vec::new) .push((&summary_id.version, &summary_id.source, info)); }; match (*old).diff(new) { edit::Edit::Copy(_) => { // Add all elements to unchanged. for (summary_id, info) in new { add_unchanged(summary_id, info); } } edit::Edit::Change(diff) => { for (summary_id, diff) in diff { match diff { edit::map::Edit::Copy(info) => { // No changes. add_unchanged(summary_id, info); } edit::map::Edit::Insert(info) => { // New package. let status = SummaryDiffStatus::Added { info }; changed.insert(summary_id, status); } edit::map::Edit::Remove(old_info) => { // Removed package. let status = SummaryDiffStatus::Removed { old_info }; changed.insert(summary_id, status); } edit::map::Edit::Change((old_info, new_info)) => { // The feature set or status changed. let status = SummaryDiffStatus::make_changed(None, None, old_info, new_info); changed.insert(summary_id, status); } } } } } // Combine lone inserts and removes into changes. Self::combine_insert_remove(&mut changed); Self { changed, unchanged } } /// Returns true if there are no changes in this diff. pub fn is_unchanged(&self) -> bool { self.changed.is_empty() } // --- // Helper methods // --- fn combine_insert_remove(changed: &mut BTreeMap<&'a SummaryId, SummaryDiffStatus<'a>>) { let mut combine_statuses = HashMap::with_capacity(changed.len()); for (summary_id, status) in &*changed { let entry = combine_statuses .entry(summary_id.name.as_str()) .or_insert_with(|| CombineStatus::None); match status { SummaryDiffStatus::Added { .. } => entry.record_added(summary_id), SummaryDiffStatus::Removed { .. } => entry.record_removed(summary_id), SummaryDiffStatus::Modified { .. } => entry.record_changed(), } } for status in combine_statuses.values() { if let CombineStatus::Combine { added, removed } = status { let removed_status = changed .remove(removed) .expect("removed ID should be present"); let old_info = match removed_status { SummaryDiffStatus::Removed { old_info } => old_info, other => panic!("expected Removed, found {:?}", other), }; let added_status = changed.get_mut(added).expect("added ID should be present"); let new_info = match &*added_status { SummaryDiffStatus::Added { info } => *info, other => panic!("expected Added, found {:?}", other), }; let old_version = if added.version != removed.version { Some(&removed.version) } else { None }; let old_source = if added.source != removed.source { Some(&removed.source) } else { None }; // Don't need the old value of added_status any more since we've already extracted the value out of it. let _ = mem::replace( added_status, SummaryDiffStatus::make_changed(old_version, old_source, old_info, new_info), ); } } } } pub(crate) fn changed_sort_key<'a>( summary_id: &'a SummaryId, status: &SummaryDiffStatus<'_>, ) -> impl Ord + 'a { // The sort order is: // * diff tag (added/modified/removed) // * package status // * summary id // TODO: allow customizing sort order? (status.tag(), status.latest_status(), summary_id) } impl<'a> Serialize for PackageDiff<'a> { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { #[derive(Serialize)] struct Changed<'a> { // Flatten both fields so that all the details show up in a single map. (This is // required for TOML.) #[serde(flatten)] package: &'a SummaryId, #[serde(flatten)] changes: &'a SummaryDiffStatus<'a>, } let mut changed: Vec = self .changed .iter() .map(|(package, changes)| Changed { package, changes }) .collect(); // The sorting ensures the order added -> modified -> removed. changed.sort_by_key(|item| changed_sort_key(item.package, item.changes)); let mut state = serializer.serialize_struct("PackageDiff", 2)?; state.serialize_field("changed", &changed)?; #[derive(Serialize)] struct Unchanged<'a> { // This matches the SummaryId format. name: &'a str, version: &'a Version, #[serde(flatten)] source: &'a SummarySource, #[serde(flatten)] info: &'a PackageInfo, } // Trying to print out an empty unchanged can cause a ValueAfterTable issue with the TOML // output. if !self.unchanged.is_empty() { let mut unchanged: Vec<_> = self .unchanged .iter() .flat_map(|(&name, info)| { info.iter().map(move |(version, source, info)| Unchanged { name, version, source, info, }) }) .collect(); // Sort by (name, version, source). unchanged.sort_by_key(|item| (item.name, item.version, item.source)); state.serialize_field("unchanged", &unchanged)?; } state.end() } } /// The diff status for a particular summary ID and source. #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "kebab-case", tag = "change")] pub enum SummaryDiffStatus<'a> { /// This package was added. #[serde(rename_all = "kebab-case")] Added { /// The information for this package. #[serde(flatten)] info: &'a PackageInfo, }, /// This package was removed. #[serde(rename_all = "kebab-case")] Removed { /// The information this package used to have. #[serde(flatten, with = "removed_impl")] old_info: &'a PackageInfo, }, /// Some details about the package changed: /// * a feature was added or removed /// * the version or source changed. #[serde(rename_all = "kebab-case")] Modified { /// The old version of this package, if the version changed. old_version: Option<&'a Version>, /// The old source of this package, if the source changed. old_source: Option<&'a SummarySource>, /// The old status of this package, if the status changed. old_status: Option, /// The current status of this package. new_status: PackageStatus, /// The set of features added to the package. added_features: BTreeSet<&'a str>, /// The set of features removed from the package. removed_features: BTreeSet<&'a str>, /// The set of features which were enabled both in both the old and new summaries. unchanged_features: BTreeSet<&'a str>, /// The set of optional dependencies added to the package. #[serde(default)] added_optional_deps: BTreeSet<&'a str>, /// The set of optional dependencies removed from the package. #[serde(default)] removed_optional_deps: BTreeSet<&'a str>, /// The set of optional dependencies enabled both in both the old and new summaries. #[serde(default)] unchanged_optional_deps: BTreeSet<&'a str>, }, } impl<'a> SummaryDiffStatus<'a> { fn make_changed( old_version: Option<&'a Version>, old_source: Option<&'a SummarySource>, old_info: &'a PackageInfo, new_info: &'a PackageInfo, ) -> Self { let old_status = if old_info.status != new_info.status { Some(old_info.status) } else { None }; let [added_features, removed_features, unchanged_features] = Self::make_changed_diff(&old_info.features, &new_info.features); let [added_optional_deps, removed_optional_deps, unchanged_optional_deps] = Self::make_changed_diff(&old_info.optional_deps, &new_info.optional_deps); SummaryDiffStatus::Modified { old_version, old_source, old_status, new_status: new_info.status, added_features, removed_features, unchanged_features, added_optional_deps, removed_optional_deps, unchanged_optional_deps, } } fn make_changed_diff( old_features: &'a BTreeSet, new_features: &'a BTreeSet, ) -> [BTreeSet<&'a str>; 3] { let mut added_features = BTreeSet::new(); let mut removed_features = BTreeSet::new(); let mut unchanged_features = BTreeSet::new(); match old_features.diff(new_features) { edit::Edit::Copy(features) => { unchanged_features.extend(features.iter().map(|feature| feature.as_str())); } edit::Edit::Change(diff) => { for (_, diff) in diff { match diff { edit::set::Edit::Copy(feature) => { unchanged_features.insert(feature.as_str()); } edit::set::Edit::Insert(feature) => { added_features.insert(feature.as_str()); } edit::set::Edit::Remove(feature) => { removed_features.insert(feature.as_str()); } } } } } [added_features, removed_features, unchanged_features] } /// Returns the tag for this status. /// /// The tag is similar to this enum, except it has no associated data. pub fn tag(&self) -> SummaryDiffTag { match self { SummaryDiffStatus::Added { .. } => SummaryDiffTag::Added, SummaryDiffStatus::Removed { .. } => SummaryDiffTag::Removed, SummaryDiffStatus::Modified { .. } => SummaryDiffTag::Modified, } } /// Returns the new package status if available, otherwise the old status. pub fn latest_status(&self) -> PackageStatus { match self { SummaryDiffStatus::Added { info } => info.status, SummaryDiffStatus::Removed { old_info } => old_info.status, SummaryDiffStatus::Modified { new_status, .. } => *new_status, } } } mod removed_impl { use super::*; use serde::Serializer; pub fn serialize(item: &PackageInfo, serializer: S) -> Result where S: Serializer, { #[derive(Serialize)] #[serde(rename_all = "kebab-case")] struct OldPackageInfo<'a> { old_status: &'a PackageStatus, old_features: &'a BTreeSet, } let old_info = OldPackageInfo { old_status: &item.status, old_features: &item.features, }; old_info.serialize(serializer) } } /// A tag representing `SummaryDiffStatus` except with no data attached. /// /// The order is significant: it is what's used as the default order in reports. #[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] pub enum SummaryDiffTag { /// This package was added. Added, /// This package was modified. Modified, /// This package was removed. Removed, } impl fmt::Display for SummaryDiffTag { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { SummaryDiffTag::Added => write!(f, "A"), SummaryDiffTag::Modified => write!(f, "M"), SummaryDiffTag::Removed => write!(f, "R"), } } } impl<'a> Diffable<'a> for PackageInfo { type Diff = (&'a PackageInfo, &'a PackageInfo); fn diff(&'a self, other: &'a Self) -> edit::Edit<'a, Self> { if self == other { edit::Edit::Copy(self) } else { edit::Edit::Change((self, other)) } } } impl<'a> Diffable<'a> for PackageStatus { type Diff = (&'a PackageStatus, &'a PackageStatus); fn diff(&'a self, other: &'a Self) -> edit::Edit<'a, Self> { if self == other { edit::Edit::Copy(self) } else { edit::Edit::Change((self, other)) } } } // Status tracker for combining inserts and removes. enum CombineStatus<'a> { None, Added(&'a SummaryId), Removed(&'a SummaryId), Combine { added: &'a SummaryId, removed: &'a SummaryId, }, Ignore, } impl<'a> CombineStatus<'a> { fn record_added(&mut self, summary_id: &'a SummaryId) { let new = match self { CombineStatus::None => CombineStatus::Added(summary_id), CombineStatus::Removed(removed) => CombineStatus::Combine { added: summary_id, removed, }, _ => CombineStatus::Ignore, }; let _ = mem::replace(self, new); } fn record_removed(&mut self, summary_id: &'a SummaryId) { let new = match self { CombineStatus::None => CombineStatus::Removed(summary_id), CombineStatus::Added(added) => CombineStatus::Combine { added, removed: summary_id, }, _ => CombineStatus::Ignore, }; let _ = mem::replace(self, new); } fn record_changed(&mut self) { // If this package name appears in the changed list at all, don't combine its // features. let _ = mem::replace(self, CombineStatus::Ignore); } } guppy-summaries-0.7.1/src/lib.rs000064400000000000000000000067111046102023000147100ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Facilities to serialize, deserialize and compare build summaries. //! //! A *build summary* is a record of what packages and features are built on the target and host //! platforms. A summary file can be checked into a repository, kept up to date and compared in CI, //! and allow for tracking results of builds over time. //! //! `guppy-summaries` is designed to be small and independent of the main `guppy` crate. //! //! # Examples //! //! ```rust //! use guppy_summaries::{Summary, SummaryId, SummarySource, PackageStatus}; //! use pretty_assertions::assert_eq; //! use semver::Version; //! use std::collections::BTreeSet; //! use toml::Value; //! //! // A summary is a TOML file that has this format: //! static SUMMARY: &str = r#" //! [[target-package]] //! name = "foo" //! version = "1.2.3" //! workspace-path = "foo" //! status = 'initial' //! features = ["feature-a", "feature-c"] //! //! [[host-package]] //! name = "proc-macro" //! version = "0.1.2" //! workspace-path = "proc-macros/macro" //! status = 'workspace' //! features = ["macro-expand"] //! //! [[host-package]] //! name = "bar" //! version = "0.4.5" //! crates-io = true //! status = 'direct' //! features = [] //! "#; //! //! // The summary can be deserialized: //! let summary = Summary::parse(SUMMARY).expect("from_str succeeded"); //! //! // ... and a package and its features can be looked up. //! let summary_id = SummaryId::new("foo", Version::new(1, 2, 3), SummarySource::workspace("foo")); //! let info = &summary.target_packages[&summary_id]; //! assert_eq!(info.status, PackageStatus::Initial, "correct status"); //! assert_eq!( //! info.features.iter().map(|feature| feature.as_str()).collect::>(), //! ["feature-a", "feature-c"], //! "correct feature list" //! ); //! //! // Another summary. //! static SUMMARY2: &str = r#" //! [[target-package]] //! name = "foo" //! version = "1.2.4" //! workspace-path = "new-location/foo" //! status = 'initial' //! features = ["feature-a", "feature-b"] //! //! [[target-package]] //! name = "once_cell" //! version = "1.4.0" //! source = "git+https://github.com/matklad/once_cell?tag=v1.4.0" //! status = 'transitive' //! features = ["std"] //! //! [[host-package]] //! name = "bar" //! version = "0.4.5" //! crates-io = true //! status = 'direct' //! features = [] //! "#; //! //! let summary2 = Summary::parse(SUMMARY2).expect("from_str succeeded"); //! //! // Diff summary and summary2. //! let diff = summary.diff(&summary2); //! //! // Pretty-print a report generated from the diff. //! let diff_str = format!("{}", diff.report()); //! assert_eq!( //! r#"target packages: //! A once_cell 1.4.0 (transitive third-party, external 'git+https://github.com/matklad/once_cell?tag=v1.4.0') //! * features: std //! M foo 1.2.4 (initial, path 'new-location/foo') //! * version upgraded from 1.2.3 //! * source changed from path 'foo' //! * added features: feature-b //! * removed features: feature-c //! * (unchanged features: feature-a) //! * (unchanged optional dependencies: [none]) //! //! host packages: //! R proc-macro 0.1.2 (workspace, path 'proc-macros/macro') //! * (old features: macro-expand) //! //! "#, //! diff_str, //! ); //! ``` #![forbid(unsafe_code)] #![warn(missing_docs)] pub mod diff; // report::SummaryReport is exported through the diff module. mod report; mod summary; #[cfg(test)] mod unit_tests; pub use summary::*; guppy-summaries-0.7.1/src/report.rs000064400000000000000000000156221046102023000154560ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::{ diff::{changed_sort_key, PackageDiff, SummaryDiff, SummaryDiffStatus}, SummaryId, }; use std::fmt; /// A report of a diff between two summaries. /// /// This report can be generated or written to a file through `fmt::Display`. #[derive(Clone, Debug)] pub struct SummaryReport<'a, 'b> { diff: &'b SummaryDiff<'a>, sorted_target: Vec<(&'a SummaryId, &'b SummaryDiffStatus<'a>)>, sorted_host: Vec<(&'a SummaryId, &'b SummaryDiffStatus<'a>)>, } impl<'a, 'b> SummaryReport<'a, 'b> { /// Creates a new `SummaryReport` that can be displayed. pub fn new(diff: &'b SummaryDiff<'a>) -> Self { let sorted_target = Self::make_sorted(&diff.target_packages); let sorted_host = Self::make_sorted(&diff.host_packages); Self { diff, sorted_target, sorted_host, } } fn make_sorted( packages: &'b PackageDiff<'a>, ) -> Vec<(&'a SummaryId, &'b SummaryDiffStatus<'a>)> { let mut v: Vec<_> = packages .changed .iter() .map(|(summary_id, status)| (*summary_id, status)) .collect(); v.sort_by_key(|(summary_id, status)| changed_sort_key(summary_id, status)); v } } impl<'a, 'b> fmt::Display for SummaryReport<'a, 'b> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if !self.diff.target_packages.is_unchanged() { writeln!( f, "target packages:\n{}", PackageReport::new(&self.diff.target_packages, &self.sorted_target) )?; } if !self.diff.host_packages.is_unchanged() { writeln!( f, "host packages:\n{}", PackageReport::new(&self.diff.host_packages, &self.sorted_host) )?; } Ok(()) } } // Collapse the lifetime params into one because three is too annoying, all the params here are // covariant anyway, and this is an internal struct. struct PackageReport<'x> { package_diff: &'x PackageDiff<'x>, sorted: &'x [(&'x SummaryId, &'x SummaryDiffStatus<'x>)], } impl<'x> PackageReport<'x> { fn new( package_diff: &'x PackageDiff<'x>, sorted: &'x [(&'x SummaryId, &'x SummaryDiffStatus<'x>)], ) -> Self { Self { package_diff, sorted, } } } impl<'x> fmt::Display for PackageReport<'x> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for (summary_id, status) in self.sorted { write!( f, " {} {} {} ({}, {})", status.tag(), summary_id.name, summary_id.version, status.latest_status(), summary_id.source )?; // Print out other versions if available. if let Some(unchanged_list) = self.package_diff.unchanged.get(summary_id.name.as_str()) { write!(f, " (other versions: ")?; display_list(f, unchanged_list.iter().map(|(version, _, _)| *version))?; write!(f, ")")?; } writeln!(f)?; match status { SummaryDiffStatus::Added { info } => { write!(f, " * features: ")?; display_list(f, &info.features)?; writeln!(f)?; } SummaryDiffStatus::Removed { old_info } => { write!(f, " * (old features: ")?; display_list(f, &old_info.features)?; writeln!(f, ")")?; } SummaryDiffStatus::Modified { old_version, old_source, old_status, // The new status is printed in the package header. new_status: _, added_features, removed_features, unchanged_features, added_optional_deps, removed_optional_deps, unchanged_optional_deps, } => { if let Some(old_version) = old_version { let change_str = if summary_id.version > **old_version { "upgraded" } else { "DOWNGRADED" }; writeln!(f, " * version {} from {}", change_str, old_version)?; } if let Some(old_source) = old_source { writeln!(f, " * source changed from {}", old_source)?; } if let Some(old_status) = old_status { writeln!(f, " * status changed from {}", old_status)?; } // --- if !added_features.is_empty() { write!(f, " * added features: ")?; display_list(f, added_features.iter().copied())?; writeln!(f)?; } if !removed_features.is_empty() { write!(f, " * removed features: ")?; display_list(f, removed_features.iter().copied())?; writeln!(f)?; } write!(f, " * (unchanged features: ")?; display_list(f, unchanged_features.iter().copied())?; writeln!(f, ")")?; // --- if !added_optional_deps.is_empty() { write!(f, " * added optional dependencies: ")?; display_list(f, added_optional_deps.iter().copied())?; writeln!(f)?; } if !removed_optional_deps.is_empty() { write!(f, " * removed optional dependencies: ")?; display_list(f, removed_optional_deps.iter().copied())?; writeln!(f)?; } write!(f, " * (unchanged optional dependencies: ")?; display_list(f, unchanged_optional_deps.iter().copied())?; writeln!(f, ")")?; } } } Ok(()) } } fn display_list(f: &mut fmt::Formatter, items: I) -> fmt::Result where I: IntoIterator, I::Item: fmt::Display, I::IntoIter: ExactSizeIterator, { let items = items.into_iter(); let len = items.len(); if len == 0 { write!(f, "[none]")?; } for (idx, item) in items.enumerate() { write!(f, "{}", item)?; // Add a comma for all items except the last one. if idx + 1 < len { write!(f, ", ")?; } } Ok(()) } guppy-summaries-0.7.1/src/summary.rs000064400000000000000000000266651046102023000156510ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::diff::SummaryDiff; use camino::{Utf8Path, Utf8PathBuf}; use semver::Version; use serde::{Deserialize, Serialize}; use std::{ collections::{BTreeMap, BTreeSet}, fmt, }; use toml::{value::Table, Serializer}; /// A type representing a package map as used in `Summary` instances. pub type PackageMap = BTreeMap; /// An in-memory representation of a build summary. /// /// The metadata parameter is customizable. /// /// For more, see the crate-level documentation. #[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)] #[serde(rename_all = "kebab-case")] pub struct Summary { /// Extra metadata associated with the summary. /// /// This may be used for storing extra information about the summary. /// /// The type defaults to `toml::Value` but is customizable. #[serde(default, skip_serializing_if = "Table::is_empty")] pub metadata: Table, /// The packages and features built on the target platform. #[serde( rename = "target-package", with = "package_map_impl", default = "PackageMap::new", skip_serializing_if = "PackageMap::is_empty" )] pub target_packages: PackageMap, /// The packages and features built on the host platform. #[serde( rename = "host-package", with = "package_map_impl", default = "PackageMap::new", skip_serializing_if = "PackageMap::is_empty" )] pub host_packages: PackageMap, } impl Summary { /// Constructs a new summary with the provided metadata, and an empty `target_packages` and /// `host_packages`. pub fn with_metadata(metadata: &impl Serialize) -> Result { let toml_str = toml::to_string(metadata)?; let metadata = toml::from_str(&toml_str).expect("toml::to_string creates a valid TOML string"); Ok(Self { metadata, ..Self::default() }) } /// Deserializes a summary from the given string, with optional custom metadata. pub fn parse(s: &str) -> Result { toml::from_str(s) } /// Perform a diff of this summary against another. /// /// This doesn't diff the metadata, just the initials and packages. pub fn diff<'a>(&'a self, other: &'a Summary) -> SummaryDiff<'a> { SummaryDiff::new(self, other) } /// Serializes this summary to a TOML string. pub fn to_string(&self) -> Result { let mut dst = String::new(); self.write_to_string(&mut dst)?; Ok(dst) } /// Serializes this summary into the given TOML string, using pretty TOML syntax. pub fn write_to_string(&self, dst: &mut String) -> Result<(), toml::ser::Error> { let mut serializer = Serializer::pretty(dst); serializer.pretty_array(false); self.serialize(&mut serializer) } } /// A unique identifier for a package in a build summary. #[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, Serialize, PartialEq, PartialOrd)] #[serde(rename_all = "kebab-case")] pub struct SummaryId { /// The name of the package. pub name: String, /// The version number of the package. pub version: Version, /// The source for this package. #[serde(flatten)] pub source: SummarySource, } impl SummaryId { /// Creates a new `SummaryId`. pub fn new(name: impl Into, version: Version, source: SummarySource) -> Self { Self { name: name.into(), version, source, } } } impl fmt::Display for SummaryId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{{ name = \"{}\", version = \"{}\", source = \"{}\"}}", self.name, self.version, self.source ) } } /// The location of a package. #[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, Serialize, PartialEq, PartialOrd)] #[serde(rename_all = "kebab-case", untagged)] pub enum SummarySource { /// A workspace path. Workspace { /// The path of this package, relative to the workspace root. #[serde( rename = "workspace-path", serialize_with = "serialize_forward_slashes" )] workspace_path: Utf8PathBuf, }, /// A non-workspace path. /// /// The path is usually relative to the workspace root, but on Windows a path that spans drives /// (e.g. a path on D:\ when the workspace root is on C:\) cannot be relative. In those cases, /// this will be the absolute path of the package. Path { /// The path of this package. #[serde(serialize_with = "serialize_forward_slashes")] path: Utf8PathBuf, }, /// The `crates.io` registry. #[serde(with = "crates_io_impl")] CratesIo, /// An external source that's not the `crates.io` registry, such as an alternate registry or /// a `git` repository. External { /// The external source. source: String, }, } impl SummarySource { /// Creates a new `SummarySource` representing a workspace source. pub fn workspace(workspace_path: impl Into) -> Self { SummarySource::Workspace { workspace_path: workspace_path.into(), } } /// Creates a new `SummarySource` representing a non-workspace path source. pub fn path(path: impl Into) -> Self { SummarySource::Path { path: path.into() } } /// Creates a new `SummarySource` representing the `crates.io` registry. pub fn crates_io() -> Self { SummarySource::CratesIo } /// Creates a new `SummarySource` representing an external source like a Git repository or a /// custom registry. pub fn external(source: impl Into) -> Self { SummarySource::External { source: source.into(), } } } impl fmt::Display for SummarySource { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { // Don't differentiate here between workspace and non-workspace paths because // PackageStatus provides that info. SummarySource::Workspace { workspace_path } => { let path_out = path_replace_slashes(workspace_path); write!(f, "path '{}'", path_out) } SummarySource::Path { path } => { let path_out = path_replace_slashes(path); write!(f, "path '{}'", path_out) } SummarySource::CratesIo => write!(f, "crates.io"), SummarySource::External { source } => write!(f, "external '{}'", source), } } } /// Information about a package in a summary that isn't part of the unique identifier. #[derive(Clone, Debug, Deserialize, Eq, Hash, Serialize, PartialEq)] #[serde(rename_all = "kebab-case")] pub struct PackageInfo { /// Where this package lies in the dependency graph. pub status: PackageStatus, /// The features built for this package. pub features: BTreeSet, /// The optional dependencies built for this package. #[serde(skip_serializing_if = "BTreeSet::is_empty", default)] pub optional_deps: BTreeSet, } /// The status of a package in a summary, such as whether it is part of the initial build set. /// /// The ordering here determines what order packages will be written out in the summary. #[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, Ord, Serialize, PartialEq, PartialOrd)] #[serde(rename_all = "kebab-case")] pub enum PackageStatus { /// This package is part of the requested build set. Initial, /// This is a workspace package that isn't part of the requested build set. Workspace, /// This package is a direct non-workspace dependency. /// /// A `Direct` package may also be transitively included. Direct, /// This package is a transitive non-workspace dependency. Transitive, } impl fmt::Display for PackageStatus { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let s = match self { PackageStatus::Initial => "initial", PackageStatus::Workspace => "workspace", PackageStatus::Direct => "direct third-party", PackageStatus::Transitive => "transitive third-party", }; write!(f, "{}", s) } } /// Serialization and deserialization for `PackageMap` instances. mod package_map_impl { use super::*; use serde::{Deserializer, Serializer}; pub fn serialize(package_map: &PackageMap, serializer: S) -> Result where S: Serializer, { // Make a list of `PackageSerialize` instances and sort by: // * status (to ensure initials come first) // * summary ID let mut package_list: Vec<_> = package_map .iter() .map(|(summary_id, info)| PackageSerialize { summary_id, info }) .collect(); package_list.sort_unstable_by_key(|package| (&package.info.status, package.summary_id)); package_list.serialize(serializer) } /// TOML representation of a package in a build summary, for serialization. #[derive(Serialize)] struct PackageSerialize<'a> { #[serde(flatten)] summary_id: &'a SummaryId, #[serde(flatten)] info: &'a PackageInfo, } pub fn deserialize<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { let packages = Vec::::deserialize(deserializer)?; let mut package_map: PackageMap = BTreeMap::new(); for package in packages { package_map.insert(package.summary_id, package.info); } Ok(package_map) } /// TOML representation of a package in a build summary, for deserialization. #[derive(Deserialize)] struct PackageDeserialize { #[serde(flatten)] summary_id: SummaryId, #[serde(flatten)] info: PackageInfo, } } /// Serializes a path with forward slashes on Windows. pub fn serialize_forward_slashes(path: &Utf8PathBuf, serializer: S) -> Result where S: serde::Serializer, { let path_out = path_replace_slashes(path); path_out.serialize(serializer) } /// Replaces backslashes with forward slashes on Windows. fn path_replace_slashes(path: &Utf8Path) -> impl fmt::Display + Serialize + '_ { // (Note: serde doesn't support non-Unicode paths anyway.) cfg_if::cfg_if! { if #[cfg(windows)] { path.as_str().replace("\\", "/") } else { path.as_str() } } } /// Serialization and deserialization for the `CratesIo` variant. mod crates_io_impl { use super::*; use serde::{de::Error, ser::SerializeMap, Deserializer, Serializer}; pub fn serialize(serializer: S) -> Result where S: Serializer, { let mut map = serializer.serialize_map(Some(1))?; map.serialize_entry("crates-io", &true)?; map.end() } pub fn deserialize<'de, D>(deserializer: D) -> Result<(), D::Error> where D: Deserializer<'de>, { let crates_io = CratesIoDeserialize::deserialize(deserializer)?; if !crates_io.crates_io { return Err(D::Error::custom("crates-io field should be true")); } Ok(()) } #[derive(Deserialize)] struct CratesIoDeserialize { #[serde(rename = "crates-io")] crates_io: bool, } } guppy-summaries-0.7.1/src/unit_tests/basic_tests.rs000064400000000000000000000425461046102023000206540ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::{ diff::SummaryDiffStatus, PackageInfo, PackageMap, PackageStatus, Summary, SummaryId, SummarySource, }; use pretty_assertions::assert_eq; use semver::Version; use std::collections::BTreeSet; static SERIALIZED_SUMMARY: &str = r#"# This is a test @generated summary. [[target-package]] name = 'foo' version = '1.2.3' workspace-path = 'foo' status = 'initial' features = ['default', 'feature1'] optional-deps = ['dep1', 'dep2'] [[target-package]] name = 'dep' version = '0.4.2' crates-io = true status = 'direct' features = ['std'] optional-deps = ['bar'] [[target-package]] name = 'no-changes' version = '1.5.3' crates-io = true status = 'transitive' features = ['default'] optional-deps = ['dep2'] [[host-package]] name = 'bar' version = '0.1.0' workspace-path = 'dir/bar' status = 'workspace' features = ['default', 'feature2'] [[host-package]] name = 'local-dep' version = '1.1.2' path = '../local-dep' status = 'transitive' features = [] optional-deps = ['dep4'] "#; static SUMMARY2: &str = r#"# This is a test @generated summary. [[target-package]] name = 'foo' version = '1.2.3' workspace-path = 'foo' status = 'initial' features = ['default', 'feature1', 'feature2'] optional-deps = ['dep1', 'dep3'] [[target-package]] name = 'dep' version = '0.4.3' crates-io = true status = 'direct' features = ['std'] optional-deps = ['bar'] [[target-package]] name = 'dep' version = '0.5.0' crates-io = true status = 'transitive' features = ['std'] [[target-package]] name = 'no-changes' version = '1.5.3' crates-io = true status = 'transitive' features = ['default'] optional-deps = ['dep2'] [[host-package]] name = 'bar' version = '0.2.0' workspace-path = 'dir/bar' status = 'initial' features = ['default', 'feature2'] [[host-package]] name = 'local-dep' version = '1.1.2' path = '../local-dep' status = 'transitive' features = ['dep-feature'] [[host-package]] name = 'local-dep' version = '2.0.0' path = '../local-dep-2' status = 'transitive' features = [] "#; #[test] fn empty_roundtrip() { let summary = Summary::default(); let mut s = "# This is a test @generated summary.\n\n".to_string(); summary.write_to_string(&mut s).expect("write succeeded"); static SERIALIZED_SUMMARY: &str = "# This is a test @generated summary.\n\n"; assert_eq!(&s, SERIALIZED_SUMMARY, "serialized representation matches"); let deserialized = Summary::parse(&s).expect("from_str succeeded"); assert_eq!(summary, deserialized, "deserialized representation matches"); let diff = summary.diff(&deserialized); assert!(diff.is_unchanged(), "diff should be empty"); } #[test] fn basic_roundtrip() { let target_packages = vec![ ( SummaryId::new( "foo", Version::new(1, 2, 3), SummarySource::workspace("foo"), ), PackageStatus::Initial, vec!["default", "feature1"], vec!["dep1", "dep2"], ), ( SummaryId::new("dep", Version::new(0, 4, 2), SummarySource::crates_io()), PackageStatus::Direct, vec!["std"], vec!["bar"], ), ( SummaryId::new( "no-changes", Version::new(1, 5, 3), SummarySource::crates_io(), ), PackageStatus::Transitive, vec!["default"], vec!["dep2"], ), ]; let host_packages = vec![ ( SummaryId::new( "bar", Version::new(0, 1, 0), SummarySource::workspace("dir/bar"), ), PackageStatus::Workspace, vec!["default", "feature2"], vec![], ), ( SummaryId::new( "local-dep", Version::new(1, 1, 2), SummarySource::path("../local-dep"), ), PackageStatus::Transitive, vec![], vec!["dep4"], ), ]; let summary = Summary { metadata: Default::default(), target_packages: make_summary(target_packages), host_packages: make_summary(host_packages), }; let mut s = "# This is a test @generated summary.\n\n".to_string(); summary.write_to_string(&mut s).expect("write succeeded"); assert_eq!(&s, SERIALIZED_SUMMARY, "serialized representation matches"); let deserialized = Summary::parse(&s).expect("from_str succeeded"); assert_eq!(summary, deserialized, "deserialized representation matches"); let diff = summary.diff(&deserialized); assert!(diff.is_unchanged(), "diff should be empty"); // Try changing some things. let summary2 = Summary::parse(SUMMARY2).expect("from_str succeeded"); let diff = summary.diff(&summary2); // target_packages is: // * a change for foo = 1 entry // * a remove + 2 inserts for dep (so it should not be combined) = 3 entries assert_eq!(diff.target_packages.changed.len(), 4, "4 changed entries"); let mut iter = diff.target_packages.changed.iter(); // First, dep 0.4.2. let std_feature: BTreeSet<_> = vec!["std".to_string()].into_iter().collect(); let bar_dep: BTreeSet<_> = vec!["bar".to_string()].into_iter().collect(); let (summary_id, status) = iter.next().expect("3 elements left"); assert_eq!(summary_id.name, "dep"); assert_eq!(summary_id.version.to_string(), "0.4.2"); assert_eq!(summary_id.source, SummarySource::crates_io()); assert_eq!( *status, SummaryDiffStatus::Removed { old_info: &PackageInfo { status: PackageStatus::Direct, features: std_feature.clone(), optional_deps: bar_dep.clone(), }, }, ); // Next, dep 0.4.3. let (summary_id, status) = iter.next().expect("2 elements left"); assert_eq!(summary_id.name, "dep"); assert_eq!(summary_id.version.to_string(), "0.4.3"); assert_eq!(summary_id.source, SummarySource::crates_io()); assert_eq!( *status, SummaryDiffStatus::Added { info: &PackageInfo { status: PackageStatus::Direct, features: std_feature.clone(), optional_deps: bar_dep, }, }, ); // Next, dep 0.5.0. let (summary_id, status) = iter.next().expect("1 element left"); assert_eq!(summary_id.name, "dep"); assert_eq!(summary_id.version.to_string(), "0.5.0"); assert_eq!(summary_id.source, SummarySource::crates_io()); assert_eq!( *status, SummaryDiffStatus::Added { info: &PackageInfo { status: PackageStatus::Transitive, features: std_feature, optional_deps: BTreeSet::new(), }, } ); // Finally, foo. let (summary_id, status) = iter.next().expect("0 elements left"); assert_eq!(summary_id.name, "foo"); assert_eq!(summary_id.version.to_string(), "1.2.3"); assert_eq!(summary_id.source, SummarySource::workspace("foo")); assert_eq!( *status, SummaryDiffStatus::Modified { old_version: None, old_source: None, old_status: None, new_status: PackageStatus::Initial, added_features: vec!["feature2"].into_iter().collect(), removed_features: BTreeSet::new(), unchanged_features: vec!["default", "feature1"].into_iter().collect(), added_optional_deps: vec!["dep3"].into_iter().collect(), removed_optional_deps: vec!["dep2"].into_iter().collect(), unchanged_optional_deps: vec!["dep1"].into_iter().collect(), } ); // host_packages is: // * an insert + remove for bar, so it *should* be combined = 1 entry // * a change + insert for local-dep, so it should not be combined = 2 entries. assert_eq!(diff.host_packages.changed.len(), 3, "3 changed entries"); let mut iter = diff.host_packages.changed.iter(); // First, bar 0.2.0. let (summary_id, status) = iter.next().expect("2 elements left"); assert_eq!(summary_id.name, "bar"); assert_eq!(summary_id.version.to_string(), "0.2.0"); assert_eq!(summary_id.source, SummarySource::workspace("dir/bar")); assert_eq!( *status, SummaryDiffStatus::Modified { old_version: Some(&Version::new(0, 1, 0)), old_source: None, old_status: Some(PackageStatus::Workspace), new_status: PackageStatus::Initial, added_features: BTreeSet::new(), removed_features: BTreeSet::new(), unchanged_features: vec!["default", "feature2"].into_iter().collect(), added_optional_deps: BTreeSet::new(), removed_optional_deps: BTreeSet::new(), unchanged_optional_deps: BTreeSet::new(), } ); // Next, local-dep 1.1.2. let (summary_id, status) = iter.next().expect("2 elements left"); assert_eq!(summary_id.name, "local-dep"); assert_eq!(summary_id.version.to_string(), "1.1.2"); assert_eq!(summary_id.source, SummarySource::path("../local-dep")); assert_eq!( *status, SummaryDiffStatus::Modified { old_version: None, old_source: None, old_status: None, new_status: PackageStatus::Transitive, added_features: vec!["dep-feature"].into_iter().collect(), removed_features: BTreeSet::new(), unchanged_features: BTreeSet::new(), added_optional_deps: BTreeSet::new(), removed_optional_deps: vec!["dep4"].into_iter().collect(), unchanged_optional_deps: BTreeSet::new(), } ); // Finally, local-dep 2.0.0. let (summary_id, status) = iter.next().expect("1 element left"); assert_eq!(summary_id.name, "local-dep"); assert_eq!(summary_id.version.to_string(), "2.0.0"); assert_eq!(summary_id.source, SummarySource::path("../local-dep-2")); assert_eq!( *status, SummaryDiffStatus::Added { info: &PackageInfo { status: PackageStatus::Transitive, features: BTreeSet::new(), optional_deps: BTreeSet::new(), }, }, ); } #[test] fn test_serialization() { let summary = Summary::parse(SERIALIZED_SUMMARY).expect("from_str succeeded"); let summary2 = Summary::parse(SUMMARY2).expect("from_str succeeded"); let diff = summary.diff(&summary2); let to_serialize = &diff; static EXPECTED_JSON: &str = indoc::indoc!( r#"{ "target-packages": { "changed": [ { "name": "dep", "version": "0.4.3", "crates-io": true, "change": "added", "status": "direct", "features": [ "std" ], "optional-deps": [ "bar" ] }, { "name": "dep", "version": "0.5.0", "crates-io": true, "change": "added", "status": "transitive", "features": [ "std" ] }, { "name": "foo", "version": "1.2.3", "workspace-path": "foo", "change": "modified", "old-version": null, "old-source": null, "old-status": null, "new-status": "initial", "added-features": [ "feature2" ], "removed-features": [], "unchanged-features": [ "default", "feature1" ], "added-optional-deps": [ "dep3" ], "removed-optional-deps": [ "dep2" ], "unchanged-optional-deps": [ "dep1" ] }, { "name": "dep", "version": "0.4.2", "crates-io": true, "change": "removed", "old-status": "direct", "old-features": [ "std" ] } ], "unchanged": [ { "name": "no-changes", "version": "1.5.3", "crates-io": true, "status": "transitive", "features": [ "default" ], "optional-deps": [ "dep2" ] } ] }, "host-packages": { "changed": [ { "name": "local-dep", "version": "2.0.0", "path": "../local-dep-2", "change": "added", "status": "transitive", "features": [] }, { "name": "bar", "version": "0.2.0", "workspace-path": "dir/bar", "change": "modified", "old-version": "0.1.0", "old-source": null, "old-status": "workspace", "new-status": "initial", "added-features": [], "removed-features": [], "unchanged-features": [ "default", "feature2" ], "added-optional-deps": [], "removed-optional-deps": [], "unchanged-optional-deps": [] }, { "name": "local-dep", "version": "1.1.2", "path": "../local-dep", "change": "modified", "old-version": null, "old-source": null, "old-status": null, "new-status": "transitive", "added-features": [ "dep-feature" ], "removed-features": [], "unchanged-features": [], "added-optional-deps": [], "removed-optional-deps": [ "dep4" ], "unchanged-optional-deps": [] } ] } }"# ); let j = serde_json::to_string_pretty(&to_serialize).expect("should serialize"); println!("json output: {}", j); assert_eq!(j, EXPECTED_JSON); static EXPECTED_TOML: &str = indoc::indoc!( r#"[[target-packages.changed]] name = "dep" version = "0.4.3" crates-io = true change = "added" status = "direct" features = ["std"] optional-deps = ["bar"] [[target-packages.changed]] name = "dep" version = "0.5.0" crates-io = true change = "added" status = "transitive" features = ["std"] [[target-packages.changed]] name = "foo" version = "1.2.3" workspace-path = "foo" change = "modified" new-status = "initial" added-features = ["feature2"] removed-features = [] unchanged-features = ["default", "feature1"] added-optional-deps = ["dep3"] removed-optional-deps = ["dep2"] unchanged-optional-deps = ["dep1"] [[target-packages.changed]] name = "dep" version = "0.4.2" crates-io = true change = "removed" old-status = "direct" old-features = ["std"] [[target-packages.unchanged]] name = "no-changes" version = "1.5.3" crates-io = true status = "transitive" features = ["default"] optional-deps = ["dep2"] [[host-packages.changed]] name = "local-dep" version = "2.0.0" path = "../local-dep-2" change = "added" status = "transitive" features = [] [[host-packages.changed]] name = "bar" version = "0.2.0" workspace-path = "dir/bar" change = "modified" old-version = "0.1.0" old-status = "workspace" new-status = "initial" added-features = [] removed-features = [] unchanged-features = ["default", "feature2"] added-optional-deps = [] removed-optional-deps = [] unchanged-optional-deps = [] [[host-packages.changed]] name = "local-dep" version = "1.1.2" path = "../local-dep" change = "modified" new-status = "transitive" added-features = ["dep-feature"] removed-features = [] unchanged-features = [] added-optional-deps = [] removed-optional-deps = ["dep4"] unchanged-optional-deps = [] "# ); let toml_out = toml::to_string(&to_serialize).expect("should serialize"); println!("toml output: {}", toml_out); assert_eq!(toml_out, EXPECTED_TOML); // TODO: add roundtrip test into the proper data structure. For now we just check that the output is valid TOML. let parsed = toml_out .parse::() .expect("deserialization from value should work"); println!("parsed output: {:?}", parsed); } fn make_summary(list: Vec<(SummaryId, PackageStatus, Vec<&str>, Vec<&str>)>) -> PackageMap { list.into_iter() .map(|(summary_id, status, features, optional_deps)| { let features = features .into_iter() .map(|feature| feature.to_string()) .collect(); let optional_deps = optional_deps .into_iter() .map(|feature| feature.to_string()) .collect(); ( summary_id, PackageInfo { status, features, optional_deps, }, ) }) .collect() } guppy-summaries-0.7.1/src/unit_tests/mod.rs000064400000000000000000000002231046102023000171120ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Unit tests for guppy-summaries. mod basic_tests;