Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ rust-version.workspace = true

[features]
diagnostics = ["tabwriter", "human_bytes"]
serde = ["dep:serde", "petgraph/serde-1"]

[dependencies]
ahash = "0.8.12"
Expand Down
10 changes: 9 additions & 1 deletion src/conflict.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<ClauseId>,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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`]
Expand All @@ -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),
Expand All @@ -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<SolvableId>,
Expand All @@ -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<ConflictNode, ConflictEdge>,
Expand Down
18 changes: 12 additions & 6 deletions src/internal/id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);

Expand All @@ -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);

Expand All @@ -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<u32>);

Expand Down Expand Up @@ -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);

Expand All @@ -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);

Expand All @@ -128,6 +130,8 @@ impl From<SolvableId> 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 {
Expand Down Expand Up @@ -241,6 +245,8 @@ impl<I: Interner> 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 {
Expand Down
4 changes: 4 additions & 0 deletions src/solver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -214,10 +216,12 @@ impl<D: DependencyProvider> Solver<D, NowOrNeverRuntime> {

/// 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<dyn Any>),
}

Expand Down
117 changes: 117 additions & 0 deletions tests/solver/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1069,3 +1069,120 @@ fn solve_for_snapshot<D: DependencyProvider>(
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"
);
}
Loading