diff --git a/src/lib.rs b/src/lib.rs index f5f3bb0..5985145 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,8 +25,9 @@ pub use internal::{ }; pub use pool::Pool; pub use solvable::Solvable; -pub use solver::{Solver, SolverCache}; +pub use solver::{Solver, SolverCache, UnsolvableOrCancelled}; use std::{ + any::Any, fmt::{Debug, Display}, hash::Hash, }; @@ -75,6 +76,16 @@ pub trait DependencyProvider: Sized { /// Returns the dependencies for the specified solvable. fn get_dependencies(&self, solvable: SolvableId) -> Dependencies; + + /// Whether the solver should stop the dependency resolution algorithm. + /// + /// This method gets called at the beginning of each unit propagation round and before + /// potentially blocking operations (like [Self::get_dependencies] and [Self::get_candidates]). + /// If it returns `Some(...)`, the solver will stop and return + /// [UnsolvableOrCancelled::Cancelled]. + fn should_cancel_with_value(&self) -> Option> { + None + } } /// A list of candidate solvables for a specific package. This is returned from diff --git a/src/problem.rs b/src/problem.rs index e15a455..311c7bd 100644 --- a/src/problem.rs +++ b/src/problem.rs @@ -65,7 +65,9 @@ impl Problem { &Clause::Requires(package_id, version_set_id) => { let package_node = Self::add_node(&mut graph, &mut nodes, package_id); - let candidates = solver.cache.get_or_cache_sorted_candidates(version_set_id); + let candidates = solver.cache.get_or_cache_sorted_candidates(version_set_id).unwrap_or_else(|_| { + unreachable!("The version set was used in the solver, so it must have been cached. Therefore cancellation is impossible here and we cannot get an `Err(...)`") + }); if candidates.is_empty() { tracing::info!( "{package_id:?} requires {version_set_id:?}, which has no candidates" diff --git a/src/solver/cache.rs b/src/solver/cache.rs index 721c341..8d25d3d 100644 --- a/src/solver/cache.rs +++ b/src/solver/cache.rs @@ -10,13 +10,14 @@ use crate::{ }; use bitvec::vec::BitVec; use elsa::FrozenMap; +use std::any::Any; use std::cell::RefCell; use std::marker::PhantomData; /// Keeps a cache of previously computed and/or requested information about solvables and version /// sets. pub struct SolverCache> { - provider: D, + pub(crate) provider: D, /// A mapping from package name to a list of candidates. candidates: Arena, @@ -70,11 +71,24 @@ impl> SolverCache &Candidates { + /// + /// If the provider has requested the solving process to be cancelled, the cancellation value + /// will be returned as an `Err(...)`. + pub fn get_or_cache_candidates( + &self, + package_name: NameId, + ) -> Result<&Candidates, Box> { // If we already have the candidates for this package cached we can simply return let candidates_id = match self.package_name_to_candidates.get_copy(&package_name) { Some(id) => id, None => { + // Since getting the candidates from the provider is a potentially blocking + // operation, we want to check beforehand whether we should cancel the solving + // process + if let Some(value) = self.provider.should_cancel_with_value() { + return Err(value); + } + // Otherwise we have to get them from the DependencyProvider let candidates = self .provider @@ -105,17 +119,23 @@ impl> SolverCache &[SolvableId] { + /// + /// If the provider has requested the solving process to be cancelled, the cancellation value + /// will be returned as an `Err(...)`. + pub fn get_or_cache_matching_candidates( + &self, + version_set_id: VersionSetId, + ) -> Result<&[SolvableId], Box> { match self.version_set_candidates.get(&version_set_id) { - Some(candidates) => candidates, + Some(candidates) => Ok(candidates), None => { let package_name = self.pool().resolve_version_set_package_name(version_set_id); let version_set = self.pool().resolve_version_set(version_set_id); - let candidates = self.get_or_cache_candidates(package_name); + let candidates = self.get_or_cache_candidates(package_name)?; let matching_candidates = candidates .candidates @@ -127,23 +147,27 @@ impl> SolverCache &[SolvableId] { + ) -> Result<&[SolvableId], Box> { match self.version_set_inverse_candidates.get(&version_set_id) { - Some(candidates) => candidates, + Some(candidates) => Ok(candidates), None => { let package_name = self.pool().resolve_version_set_package_name(version_set_id); let version_set = self.pool().resolve_version_set(version_set_id); - let candidates = self.get_or_cache_candidates(package_name); + let candidates = self.get_or_cache_candidates(package_name)?; let matching_candidates = candidates .candidates @@ -155,23 +179,30 @@ impl> SolverCache &[SolvableId] { + /// + /// If the provider has requested the solving process to be cancelled, the cancellation value + /// will be returned as an `Err(...)`. + pub fn get_or_cache_sorted_candidates( + &self, + version_set_id: VersionSetId, + ) -> Result<&[SolvableId], Box> { match self.version_set_to_sorted_candidates.get(&version_set_id) { - Some(canidates) => canidates, + Some(candidates) => Ok(candidates), None => { let package_name = self.pool().resolve_version_set_package_name(version_set_id); - let matching_candidates = self.get_or_cache_matching_candidates(version_set_id); - let candidates = self.get_or_cache_candidates(package_name); + let matching_candidates = self.get_or_cache_matching_candidates(version_set_id)?; + let candidates = self.get_or_cache_candidates(package_name)?; - // Sort all the candidates in order in which they should betried by the solver. + // Sort all the candidates in order in which they should be tried by the solver. let mut sorted_candidates = Vec::new(); sorted_candidates.extend_from_slice(matching_candidates); self.provider.sort_candidates(self, &mut sorted_candidates); @@ -185,18 +216,32 @@ impl> SolverCache &Dependencies { + /// + /// If the provider has requested the solving process to be cancelled, the cancellation value + /// will be returned as an `Err(...)`. + pub fn get_or_cache_dependencies( + &self, + solvable_id: SolvableId, + ) -> Result<&Dependencies, Box> { let dependencies_id = match self.solvable_to_dependencies.get_copy(&solvable_id) { Some(id) => id, None => { + // Since getting the dependencies from the provider is a potentially blocking + // operation, we want to check beforehand whether we should cancel the solving + // process + if let Some(value) = self.provider.should_cancel_with_value() { + return Err(value); + } + let dependencies = self.provider.get_dependencies(solvable_id); let dependencies_id = self.solvable_dependencies.alloc(dependencies); self.solvable_to_dependencies @@ -205,7 +250,7 @@ impl> SolverCache> Solver), +} + +impl From for UnsolvableOrCancelled { + fn from(value: Problem) -> Self { + UnsolvableOrCancelled::Unsolvable(value) + } +} + +impl From> for UnsolvableOrCancelled { + fn from(value: Box) -> Self { + UnsolvableOrCancelled::Cancelled(value) + } +} + +/// An error during the propagation step +pub(crate) enum PropagationError { + Conflict(SolvableId, bool, ClauseId), + Cancelled(Box), +} + impl> Solver { /// Solves the provided `jobs` and returns a transaction from the found solution /// @@ -84,7 +112,7 @@ impl> Sol pub fn solve( &mut self, root_requirements: Vec, - ) -> Result, Problem> { + ) -> Result, UnsolvableOrCancelled> { // Clear state self.decision_tracker.clear(); self.negative_assertions.clear(); @@ -141,12 +169,15 @@ impl> Sol /// solvables. /// /// Returns the added clauses, and an additional list with conflicting clauses (if any). + /// + /// If the provider has requested the solving process to be cancelled, the cancellation value + /// will be returned as an `Err(...)`. fn add_clauses_for_solvable( &mut self, solvable_id: SolvableId, - ) -> (Vec, Vec) { + ) -> Result<(Vec, Vec), Box> { if self.clauses_added_for_solvable.contains(&solvable_id) { - return (Vec::new(), Vec::new()); + return Ok((Vec::new(), Vec::new())); } let mut new_clauses = Vec::new(); @@ -169,7 +200,7 @@ impl> Sol let (requirements, constrains) = match solvable.inner { SolvableInner::Root => (self.root_requirements.clone(), Vec::new()), SolvableInner::Package(_) => { - let deps = self.cache.get_or_cache_dependencies(solvable_id); + let deps = self.cache.get_or_cache_dependencies(solvable_id)?; match deps { Dependencies::Known(deps) => { (deps.requirements.clone(), deps.constrains.clone()) @@ -192,7 +223,7 @@ impl> Sol // The new assertion should be kept (it is returned in the lhs of the // tuple), but it should also be marked as the source of a conflict (rhs // of the tuple) - return (vec![clause_id], vec![clause_id]); + return Ok((vec![clause_id], vec![clause_id])); } } } @@ -201,10 +232,10 @@ impl> Sol // Add clauses for the requirements for version_set_id in requirements { let dependency_name = self.pool().resolve_version_set_package_name(version_set_id); - self.add_clauses_for_package(dependency_name); + self.add_clauses_for_package(dependency_name)?; // Find all the solvables that match for the given version set - let candidates = self.cache.get_or_cache_sorted_candidates(version_set_id); + let candidates = self.cache.get_or_cache_sorted_candidates(version_set_id)?; // Queue requesting the dependencies of the candidates as well if they are cheaply // available from the dependency provider. @@ -241,12 +272,12 @@ impl> Sol // Add clauses for the constraints for version_set_id in constrains { let dependency_name = self.pool().resolve_version_set_package_name(version_set_id); - self.add_clauses_for_package(dependency_name); + self.add_clauses_for_package(dependency_name)?; // Find all the solvables that match for the given version set let constrained_candidates = self .cache - .get_or_cache_non_matching_candidates(version_set_id); + .get_or_cache_non_matching_candidates(version_set_id)?; // Add forbidden clauses for the candidates for forbidden_candidate in constrained_candidates.iter().copied().collect_vec() { @@ -271,7 +302,7 @@ impl> Sol self.clauses_added_for_solvable.insert(solvable_id); } - (new_clauses, conflicting_clauses) + Ok((new_clauses, conflicting_clauses)) } /// Adds all clauses for a specific package name. @@ -287,9 +318,12 @@ impl> Sol /// There is no need to propagate after adding these clauses because none of the clauses are /// assertions (only a single literal) and we assume that no decision has been made about any /// of the solvables involved. This assumption is checked when debug_assertions are enabled. - fn add_clauses_for_package(&mut self, package_name: NameId) { + /// + /// If the provider has requested the solving process to be cancelled, the cancellation value + /// will be returned as an `Err(...)`. + fn add_clauses_for_package(&mut self, package_name: NameId) -> Result<(), Box> { if self.clauses_added_for_package.contains(&package_name) { - return; + return Ok(()); } tracing::trace!( @@ -297,7 +331,7 @@ impl> Sol self.pool().resolve_package_name(package_name) ); - let package_candidates = self.cache.get_or_cache_candidates(package_name); + let package_candidates = self.cache.get_or_cache_candidates(package_name)?; let locked_solvable_id = package_candidates.locked; let candidates = &package_candidates.candidates; @@ -350,6 +384,7 @@ impl> Sol } self.clauses_added_for_package.insert(package_name); + Ok(()) } /// Run the CDCL algorithm to solve the SAT problem @@ -373,7 +408,7 @@ impl> Sol /// [`Solver::analyze`] for the implementation of this step. /// /// The solver loop can be found in [`Solver::resolve_dependencies`]. - fn run_sat(&mut self) -> Result<(), Problem> { + fn run_sat(&mut self) -> Result<(), UnsolvableOrCancelled> { assert!(self.decision_tracker.is_empty()); let mut level = 0; @@ -402,26 +437,39 @@ impl> Sol // Add the clauses for the root solvable. let (mut clauses, conflicting_clauses) = - self.add_clauses_for_solvable(SolvableId::root()); + self.add_clauses_for_solvable(SolvableId::root())?; if let Some(clause_id) = conflicting_clauses.into_iter().next() { - return Err(self.analyze_unsolvable(clause_id)); + return Err(UnsolvableOrCancelled::Unsolvable( + self.analyze_unsolvable(clause_id), + )); } new_clauses.append(&mut clauses); } // Propagate decisions from assignments above let propagate_result = self.propagate(level); - if let Err((_, _, clause_id)) = propagate_result { - if level == 1 { - return Err(self.analyze_unsolvable(clause_id)); - } else { - // When the level is higher than 1, that means the conflict was caused because - // new clauses have been added dynamically. We need to start over. - tracing::debug!("├─ added clause {clause:?} introduces a conflict which invalidates the partial solution", + + // Handle propagation errors + match propagate_result { + Ok(()) => {} + Err(PropagationError::Conflict(_, _, clause_id)) => { + if level == 1 { + return Err(UnsolvableOrCancelled::Unsolvable( + self.analyze_unsolvable(clause_id), + )); + } else { + // The conflict was caused because new clauses have been added dynamically. + // We need to start over. + tracing::debug!("├─ added clause {clause:?} introduces a conflict which invalidates the partial solution", clause=self.clauses[clause_id].debug(self.pool())); - level = 0; - self.decision_tracker.clear(); - continue; + level = 0; + self.decision_tracker.clear(); + continue; + } + } + Err(PropagationError::Cancelled(value)) => { + // Propagation was cancelled + return Err(UnsolvableOrCancelled::Cancelled(value)); } } @@ -464,7 +512,7 @@ impl> Sol for (solvable, _) in new_solvables { // Add the clauses for this particular solvable. let (mut clauses_for_solvable, conflicting_causes) = - self.add_clauses_for_solvable(solvable); + self.add_clauses_for_solvable(solvable)?; new_clauses.append(&mut clauses_for_solvable); for &clause_id in &conflicting_causes { @@ -490,7 +538,7 @@ impl> Sol /// The next variable to assign is obtained by finding the next dependency for which no concrete /// package has been picked yet. Then we pick the highest possible version for that package, or /// the favored version if it was provided by the user, and set its value to true. - fn resolve_dependencies(&mut self, mut level: u32) -> Result { + fn resolve_dependencies(&mut self, mut level: u32) -> Result { loop { // Make a decision. If no decision could be made it means the problem is satisfyable. let Some((candidate, required_by, clause_id)) = self.decide() else { @@ -586,7 +634,7 @@ impl> Sol solvable: SolvableId, required_by: SolvableId, clause_id: ClauseId, - ) -> Result { + ) -> Result { level += 1; tracing::info!( @@ -603,24 +651,33 @@ impl> Sol self.propagate_and_learn(level) } - fn propagate_and_learn(&mut self, mut level: u32) -> Result { + fn propagate_and_learn(&mut self, mut level: u32) -> Result { loop { - let r = self.propagate(level); - let Err((conflicting_solvable, attempted_value, conflicting_clause)) = r else { - // Propagation succeeded - tracing::debug!("╘══ Propagation succeeded"); - break; - }; - - level = self.learn_from_conflict( - level, - conflicting_solvable, - attempted_value, - conflicting_clause, - )?; + match self.propagate(level) { + Ok(()) => { + // Propagation completed + tracing::debug!("╘══ Propagation completed"); + return Ok(level); + } + Err(PropagationError::Cancelled(value)) => { + // Propagation cancelled + tracing::debug!("╘══ Propagation cancelled"); + return Err(UnsolvableOrCancelled::Cancelled(value)); + } + Err(PropagationError::Conflict( + conflicting_solvable, + attempted_value, + conflicting_clause, + )) => { + level = self.learn_from_conflict( + level, + conflicting_solvable, + attempted_value, + conflicting_clause, + )?; + } + } } - - Ok(level) } fn learn_from_conflict( @@ -702,7 +759,11 @@ impl> Sol /// is assigned to a solvable, each of the clauses tracking that solvable will be notified. That /// way, the clause can check whether the literal that is using the solvable has become false, in /// which case it picks a new solvable to watch (if available) or triggers an assignment. - fn propagate(&mut self, level: u32) -> Result<(), (SolvableId, bool, ClauseId)> { + fn propagate(&mut self, level: u32) -> Result<(), PropagationError> { + if let Some(value) = self.cache.provider.should_cancel_with_value() { + return Err(PropagationError::Cancelled(value)); + }; + // Negative assertions derived from other rules (assertions are clauses that consist of a // single literal, and therefore do not have watches) for &(solvable_id, clause_id) in &self.negative_assertions { @@ -710,7 +771,7 @@ impl> Sol let decided = self .decision_tracker .try_add_decision(Decision::new(solvable_id, value, clause_id), level) - .map_err(|_| (solvable_id, value, clause_id))?; + .map_err(|_| PropagationError::Conflict(solvable_id, value, clause_id))?; if decided { tracing::trace!( @@ -745,7 +806,9 @@ impl> Sol Decision::new(literal.solvable_id, decision, clause_id), level, ) - .map_err(|_| (literal.solvable_id, decision, clause_id))?; + .map_err(|_| { + PropagationError::Conflict(literal.solvable_id, decision, clause_id) + })?; if decided { tracing::trace!( @@ -833,7 +896,13 @@ impl> Sol ), level, ) - .map_err(|_| (remaining_watch.solvable_id, true, this_clause_id))?; + .map_err(|_| { + PropagationError::Conflict( + remaining_watch.solvable_id, + true, + this_clause_id, + ) + })?; if decided { match clause.kind { diff --git a/tests/snapshots/solver__resolve_and_cancel.snap b/tests/snapshots/solver__resolve_and_cancel.snap new file mode 100644 index 0000000..1522969 --- /dev/null +++ b/tests/snapshots/solver__resolve_and_cancel.snap @@ -0,0 +1,5 @@ +--- +source: tests/solver.rs +expression: error +--- +cancelled! diff --git a/tests/solver.rs b/tests/solver.rs index a0f0a01..e9fc230 100644 --- a/tests/solver.rs +++ b/tests/solver.rs @@ -2,9 +2,12 @@ use indexmap::IndexMap; use itertools::Itertools; use resolvo::{ range::Range, Candidates, DefaultSolvableDisplay, Dependencies, DependencyProvider, - KnownDependencies, NameId, Pool, SolvableId, Solver, SolverCache, VersionSet, VersionSetId, + KnownDependencies, NameId, Pool, SolvableId, Solver, SolverCache, UnsolvableOrCancelled, + VersionSet, VersionSetId, }; use std::{ + any::Any, + cell::Cell, collections::HashMap, fmt::{Debug, Display, Formatter}, io::stderr, @@ -32,39 +35,44 @@ use tracing_test::traced_test; struct Pack { version: u32, unknown_deps: bool, + cancel_during_get_dependencies: bool, } impl Pack { - fn with_version(version: u32) -> Pack { + fn new(version: u32) -> Pack { Pack { version, unknown_deps: false, + cancel_during_get_dependencies: false, } } + fn with_unknown_deps(mut self) -> Pack { + self.unknown_deps = true; + self + } + + fn cancel_during_get_dependencies(mut self) -> Pack { + self.cancel_during_get_dependencies = true; + self + } + fn offset(&self, version_offset: i32) -> Pack { - Pack { - version: self.version.wrapping_add_signed(version_offset), - unknown_deps: self.unknown_deps, - } + let mut pack = self.clone(); + pack.version = pack.version.wrapping_add_signed(version_offset); + pack } } impl From for Pack { fn from(value: u32) -> Self { - Pack { - version: value, - unknown_deps: false, - } + Pack::new(value) } } impl From for Pack { fn from(value: i32) -> Self { - Pack { - version: value as u32, - unknown_deps: false, - } + Pack::new(value as u32) } } @@ -78,7 +86,7 @@ impl FromStr for Pack { type Err = ParseIntError; fn from_str(s: &str) -> Result { - u32::from_str(s).map(Pack::with_version) + u32::from_str(s).map(Pack::new) } } @@ -136,6 +144,7 @@ struct BundleBoxProvider { favored: HashMap, locked: HashMap, excluded: HashMap>, + cancel_solving: Cell, } struct BundleBoxPackageDependencies { @@ -163,26 +172,26 @@ impl BundleBoxProvider { pub fn from_packages(packages: &[(&str, u32, Vec<&str>)]) -> Self { let mut result = Self::new(); for (name, version, deps) in packages { - result.add_package(name, Pack::with_version(*version), deps, &[]); + result.add_package(name, Pack::new(*version), deps, &[]); } result } pub fn set_favored(&mut self, package_name: &str, version: u32) { self.favored - .insert(package_name.to_owned(), Pack::with_version(version)); + .insert(package_name.to_owned(), Pack::new(version)); } pub fn exclude(&mut self, package_name: &str, version: u32, reason: impl Into) { self.excluded .entry(package_name.to_owned()) .or_default() - .insert(Pack::with_version(version), reason.into()); + .insert(Pack::new(version), reason.into()); } pub fn set_locked(&mut self, package_name: &str, version: u32) { self.locked - .insert(package_name.to_owned(), Pack::with_version(version)); + .insert(package_name.to_owned(), Pack::new(version)); } pub fn add_package( @@ -270,6 +279,12 @@ impl DependencyProvider> for BundleBoxProvider { let package_name = self.pool.resolve_package_name(candidate.name_id()); let pack = candidate.inner(); + if pack.cancel_during_get_dependencies { + self.cancel_solving.set(true); + let reason = self.pool.intern_string("cancelled"); + return Dependencies::Unknown(reason); + } + if pack.unknown_deps { let reason = self.pool.intern_string("could not retrieve deps"); return Dependencies::Unknown(reason); @@ -297,6 +312,14 @@ impl DependencyProvider> for BundleBoxProvider { Dependencies::Known(result) } + + fn should_cancel_with_value(&self) -> Option> { + if self.cancel_solving.get() { + Some(Box::new("cancelled!".to_string())) + } else { + None + } + } } /// Create a string from a [`Transaction`] @@ -321,7 +344,7 @@ fn solve_unsat(provider: BundleBoxProvider, specs: &[&str]) -> String { let mut solver = Solver::new(provider); match solver.solve(requirements) { Ok(_) => panic!("expected unsat, but a solution was found"), - Err(problem) => { + Err(UnsolvableOrCancelled::Unsolvable(problem)) => { // Write the problem graphviz to stderr let graph = problem.graph(&solver); let mut output = stderr(); @@ -334,6 +357,7 @@ fn solve_unsat(provider: BundleBoxProvider, specs: &[&str]) -> String { .display_user_friendly(&solver, &DefaultSolvableDisplay) .to_string() } + Err(UnsolvableOrCancelled::Cancelled(reason)) => *reason.downcast().unwrap(), } } @@ -343,7 +367,7 @@ fn solve_snapshot(provider: BundleBoxProvider, specs: &[&str]) -> String { let mut solver = Solver::new(provider); match solver.solve(requirements) { Ok(solvables) => transaction_to_string(solver.pool(), &solvables), - Err(problem) => { + Err(UnsolvableOrCancelled::Unsolvable(problem)) => { // Write the problem graphviz to stderr let graph = problem.graph(&solver); let mut output = stderr(); @@ -356,6 +380,7 @@ fn solve_snapshot(provider: BundleBoxProvider, specs: &[&str]) -> String { .display_user_friendly(&solver, &DefaultSolvableDisplay) .to_string() } + Err(UnsolvableOrCancelled::Cancelled(reason)) => *reason.downcast().unwrap(), } } @@ -451,17 +476,8 @@ fn test_resolve_with_conflict() { ("conflicting", 1, vec![]), ("conflicting", 0, vec![]), ]); - let requirements = provider.requirements(&["asdf", "efgh"]); - let mut solver = Solver::new(provider); - let solved = solver.solve(requirements); - let solved = match solved { - Ok(solved) => transaction_to_string(solver.pool(), &solved), - Err(p) => panic!( - "{}", - p.display_user_friendly(&solver, &DefaultSolvableDisplay) - ), - }; - insta::assert_snapshot!(solved); + let result = solve_snapshot(provider, &["asdf", "efgh"]); + insta::assert_snapshot!(result); } /// The non-existing package should not be selected @@ -530,22 +546,11 @@ fn test_resolve_with_unknown_deps() { let mut provider = BundleBoxProvider::new(); provider.add_package( "opentelemetry-api", - Pack { - version: 3, - unknown_deps: true, - }, - &[], - &[], - ); - provider.add_package( - "opentelemetry-api", - Pack { - version: 2, - unknown_deps: false, - }, + Pack::new(3).with_unknown_deps(), &[], &[], ); + provider.add_package("opentelemetry-api", Pack::new(2), &[], &[]); let requirements = provider.requirements(&["opentelemetry-api"]); let mut solver = Solver::new(provider); let solved = solver.solve(requirements).unwrap(); @@ -561,6 +566,26 @@ fn test_resolve_with_unknown_deps() { assert_eq!(solvable.inner().version, 2); } +#[test] +#[traced_test] +fn test_resolve_and_cancel() { + let mut provider = BundleBoxProvider::new(); + provider.add_package( + "opentelemetry-api", + Pack::new(3).with_unknown_deps(), + &[], + &[], + ); + provider.add_package( + "opentelemetry-api", + Pack::new(2).cancel_during_get_dependencies(), + &[], + &[], + ); + let error = solve_unsat(provider, &["opentelemetry-api"]); + insta::assert_snapshot!(error); +} + /// Locking a specific package version in this case a lower version namely `3` should result /// in the higher package not being considered #[test] @@ -619,20 +644,8 @@ fn test_resolve_favor_without_conflict() { provider.set_favored("a", 1); provider.set_favored("b", 1); - let requirements = provider.requirements(&["a", "b 2"]); - // Already installed: A=1; B=1 - let mut solver = Solver::new(provider); - let solved = solver.solve(requirements); - let solved = match solved { - Ok(solved) => solved, - Err(p) => panic!( - "{}", - p.display_user_friendly(&solver, &DefaultSolvableDisplay) - ), - }; - - let result = transaction_to_string(&solver.pool(), &solved); + let result = solve_snapshot(provider, &["a", "b 2"]); insta::assert_snapshot!(result, @r###" a=1 b=2 @@ -653,19 +666,7 @@ fn test_resolve_favor_with_conflict() { provider.set_favored("b", 1); provider.set_favored("c", 1); - let requirements = provider.requirements(&["a", "b 2"]); - - let mut solver = Solver::new(provider); - let solved = solver.solve(requirements); - let solved = match solved { - Ok(solved) => solved, - Err(p) => panic!( - "{}", - p.display_user_friendly(&solver, &DefaultSolvableDisplay) - ), - }; - - let result = transaction_to_string(&solver.pool(), &solved); + let result = solve_snapshot(provider, &["a", "b 2"]); insta::assert_snapshot!(result, @r###" a=2 b=2