diff --git a/Cargo.lock b/Cargo.lock index a15833c..ff0417a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -883,6 +883,7 @@ dependencies = [ "hashbrown", "indexmap", "serde", + "serde_derive", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 45da277..8a09ae6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ rust-version.workspace = true [features] diagnostics = ["tabwriter", "human_bytes"] +serde = ["dep:serde", "petgraph/serde-1"] [dependencies] ahash = "0.8.12" diff --git a/src/conflict.rs b/src/conflict.rs index fd2cfa4..e7ee0ac 100644 --- a/src/conflict.rs +++ b/src/conflict.rs @@ -10,6 +10,8 @@ use petgraph::{ graph::{DiGraph, EdgeIndex, EdgeReference, NodeIndex}, visit::{Bfs, DfsPostOrder, EdgeRef}, }; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; use crate::{ DependencyProvider, Interner, Requirement, @@ -22,7 +24,8 @@ use crate::{ }; /// Represents the cause of the solver being unable to find a solution -#[derive(Debug)] +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Conflict { /// The clauses involved in an unsatisfiable conflict clauses: Vec, @@ -218,6 +221,7 @@ impl Conflict { /// A node in the graph representation of a [`Conflict`] #[derive(Copy, Clone, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ConflictNode { /// Node corresponding to a solvable Solvable(SolvableOrRootId), @@ -247,6 +251,7 @@ impl ConflictNode { /// An edge in the graph representation of a [`Conflict`] #[derive(Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ConflictEdge { /// The target node is a candidate for the dependency specified by the /// [`Requirement`] @@ -273,6 +278,7 @@ impl ConflictEdge { /// Conflict causes #[derive(Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ConflictCause { /// The solvable is locked Locked(SolvableId), @@ -292,6 +298,7 @@ pub enum ConflictCause { /// - They all have the same name /// - They all have the same predecessor nodes /// - They all have the same successor nodes +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct MergedConflictNode { /// The list of solvable ids that have been merged into this node. pub ids: Vec, @@ -303,6 +310,7 @@ pub struct MergedConflictNode { /// solvable's requirements are included in the graph, only those that are /// directly or indirectly involved in the conflict. #[derive(Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct ConflictGraph { /// The conflict graph as a directed petgraph. pub graph: DiGraph, diff --git a/src/internal/id.rs b/src/internal/id.rs index e59021d..71687e8 100644 --- a/src/internal/id.rs +++ b/src/internal/id.rs @@ -4,11 +4,13 @@ use std::{ }; use crate::{Interner, internal::arena::ArenaId}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; /// The id associated to a package name #[repr(transparent)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(transparent))] pub struct NameId(pub u32); @@ -25,7 +27,7 @@ impl ArenaId for NameId { /// The id associated with a generic string #[repr(transparent)] #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(transparent))] pub struct StringId(pub u32); @@ -42,7 +44,7 @@ impl ArenaId for StringId { /// The id associated with a VersionSet. #[repr(transparent)] #[derive(Clone, Default, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(transparent))] pub struct VersionSetId(pub u32); @@ -59,7 +61,7 @@ impl ArenaId for VersionSetId { /// The id associated with a Condition. #[repr(transparent)] #[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(transparent))] pub struct ConditionId(NonZero); @@ -89,7 +91,7 @@ impl ArenaId for ConditionId { /// The id associated with a union (logical OR) of two or more version sets. #[repr(transparent)] #[derive(Clone, Default, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(transparent))] pub struct VersionSetUnionId(pub u32); @@ -106,7 +108,7 @@ impl ArenaId for VersionSetUnionId { /// The id associated to a solvable #[repr(transparent)] #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(transparent))] pub struct SolvableId(pub u32); @@ -128,6 +130,8 @@ impl From for u32 { #[repr(transparent)] #[derive(Copy, Clone, PartialOrd, Ord, Eq, PartialEq, Debug, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] pub(crate) struct ClauseId(NonZeroU32); impl ClauseId { @@ -241,6 +245,8 @@ impl Display for DisplaySolvableId<'_, I> { #[repr(transparent)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] pub struct SolvableOrRootId(u32); impl SolvableOrRootId { diff --git a/src/solver/mod.rs b/src/solver/mod.rs index 8e0e25e..0ce9621 100644 --- a/src/solver/mod.rs +++ b/src/solver/mod.rs @@ -10,6 +10,8 @@ use elsa::FrozenMap; use encoding::Encoder; use indexmap::IndexMap; use itertools::Itertools; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; use variable_map::VariableMap; use watch_map::WatchMap; @@ -214,10 +216,12 @@ impl Solver { /// The root cause of a solver error. #[derive(Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum UnsolvableOrCancelled { /// The problem was unsolvable. Unsolvable(Conflict), /// The solving process was cancelled. + #[cfg_attr(feature = "serde", serde(skip))] Cancelled(Box), } diff --git a/tests/solver/main.rs b/tests/solver/main.rs index ec8b32f..c81353d 100644 --- a/tests/solver/main.rs +++ b/tests/solver/main.rs @@ -1069,3 +1069,120 @@ fn solve_for_snapshot( Err(UnsolvableOrCancelled::Cancelled(reason)) => *reason.downcast().unwrap(), } } + +#[cfg(feature = "serde")] +#[test] +fn test_conflict_serialization_json() { + let mut provider = BundleBoxProvider::from_packages(&[ + ("a", 1, vec!["b"]), + ("b", 1, vec!["c 2"]), + ("c", 1, vec![]), + ]); + + let requirements = provider.requirements(&["a"]); + let mut solver = Solver::new(provider); + let problem = Problem::new().requirements(requirements); + + let Err(UnsolvableOrCancelled::Unsolvable(conflict)) = solver.solve(problem) else { + panic!("Expected an unsolvable conflict"); + }; + + let json = serde_json::to_string(&conflict).expect("Failed to serialize conflict to JSON"); + let _deserialized_conflict: resolvo::conflict::Conflict = + serde_json::from_str(&json).expect("Failed to deserialize conflict from JSON"); + let graph = conflict.graph(&solver); + let _graph_json = + serde_json::to_string(&graph).expect("Failed to serialize ConflictGraph to JSON"); + let unsolvable = UnsolvableOrCancelled::Unsolvable(conflict); + let _unsolvable_json = + serde_json::to_string(&unsolvable).expect("Failed to serialize UnsolvableOrCancelled"); + let parsed: serde_json::Value = serde_json::from_str(&json).expect("Invalid JSON"); + assert!( + parsed.get("clauses").is_some(), + "Conflict should have clauses field" + ); +} + +#[cfg(feature = "serde")] +#[test] +fn test_multiple_error_scenarios() { + let mut provider = BundleBoxProvider::from_packages(&[("foo", 1, vec!["nonexistent"])]); + + let requirements = provider.requirements(&["foo"]); + let mut solver = Solver::new(provider); + let problem = Problem::new().requirements(requirements); + + let Err(UnsolvableOrCancelled::Unsolvable(conflict)) = solver.solve(problem) else { + panic!("Expected unsolvable conflict in scenario 1"); + }; + + let json = serde_json::to_string(&conflict).expect("Scenario 1: JSON serialization failed"); + let _: resolvo::conflict::Conflict = + serde_json::from_str(&json).expect("Scenario 1: JSON deserialization failed"); + + let mut provider = BundleBoxProvider::from_packages(&[("x", 1, vec!["y 2"]), ("y", 1, vec![])]); + + let requirements = provider.requirements(&["x"]); + let mut solver = Solver::new(provider); + let problem = Problem::new().requirements(requirements); + + let Err(UnsolvableOrCancelled::Unsolvable(conflict)) = solver.solve(problem) else { + panic!("Expected unsolvable conflict in scenario 2"); + }; + + let json = serde_json::to_string(&conflict).expect("Scenario 2: JSON serialization failed"); + let _: resolvo::conflict::Conflict = + serde_json::from_str(&json).expect("Scenario 2: JSON deserialization failed"); +} + +#[cfg(feature = "serde")] +#[test] +fn test_serialization_formats() { + let mut provider = BundleBoxProvider::from_packages(&[("test", 1, vec!["missing"])]); + + let requirements = provider.requirements(&["test"]); + let mut solver = Solver::new(provider); + let problem = Problem::new().requirements(requirements); + + let Err(UnsolvableOrCancelled::Unsolvable(conflict)) = solver.solve(problem) else { + panic!("Expected unsolvable conflict"); + }; + + let json = serde_json::to_string(&conflict).expect("JSON serialization failed"); + let _: resolvo::conflict::Conflict = + serde_json::from_str(&json).expect("JSON deserialization failed"); + let pretty_json = + serde_json::to_string_pretty(&conflict).expect("Pretty JSON serialization failed"); + let _: resolvo::conflict::Conflict = + serde_json::from_str(&pretty_json).expect("Pretty JSON deserialization failed"); + let value: serde_json::Value = + serde_json::to_value(&conflict).expect("Value conversion failed"); + let _: resolvo::conflict::Conflict = + serde_json::from_value(value).expect("Value deserialization failed"); +} + +#[cfg(feature = "serde")] +#[test] +fn test_unsolvable_or_cancelled_enum_structure() { + let mut provider = BundleBoxProvider::from_packages(&[("test", 1, vec!["missing"])]); + + let requirements = provider.requirements(&["test"]); + let mut solver = Solver::new(provider); + let problem = Problem::new().requirements(requirements); + + let Err(UnsolvableOrCancelled::Unsolvable(conflict)) = solver.solve(problem) else { + panic!("Expected unsolvable conflict"); + }; + + let unsolvable = UnsolvableOrCancelled::Unsolvable(conflict); + let json = serde_json::to_string(&unsolvable).expect("Serialization failed"); + let parsed: serde_json::Value = serde_json::from_str(&json).expect("Invalid JSON"); + assert!( + parsed.get("Unsolvable").is_some(), + "Should contain Unsolvable variant" + ); + assert!( + parsed.get("Cancelled").is_none(), + "Should not contain Cancelled variant when serializing Unsolvable" + ); +}