diff options
Diffstat (limited to 'crates/mozart-core/src/dependency_resolver/solver.rs')
| -rw-r--r-- | crates/mozart-core/src/dependency_resolver/solver.rs | 1008 |
1 files changed, 1008 insertions, 0 deletions
diff --git a/crates/mozart-core/src/dependency_resolver/solver.rs b/crates/mozart-core/src/dependency_resolver/solver.rs new file mode 100644 index 0000000..4abb888 --- /dev/null +++ b/crates/mozart-core/src/dependency_resolver/solver.rs @@ -0,0 +1,1008 @@ +use super::decisions::Decisions; +use super::error::{SolverBugError, SolverError}; +use super::policy::DefaultPolicy; +use super::pool::{Literal, PackageId, Pool, literal_to_package_id}; +use super::problem::Problem; +use super::rule::{ReasonData, Rule, RuleReason, RuleType}; +use super::rule_set::{RuleId, RuleSet}; +use super::rule_watch_graph::RuleWatchGraph; +use indexmap::{IndexMap, IndexSet}; + +/// Result of solving: the list of package IDs to install. +#[derive(Debug)] +pub struct SolverResult { + /// Package IDs decided for installation. + pub installed: Vec<PackageId>, +} + +/// Main SAT solver implementing CDCL (Conflict-Driven Clause Learning). +/// +/// Port of Composer's Solver.php. +pub struct Solver<'a> { + pool: &'a Pool, + policy: DefaultPolicy, + rules: RuleSet, + watch_graph: RuleWatchGraph, + decisions: Decisions, + /// Fixed packages by ID. + fixed_map: IndexSet<PackageId>, + /// Current propagation index in decision queue. + propagate_index: usize, + /// Branch points: (alternative literals, decision level). + branches: Vec<(Vec<Literal>, i32)>, + /// Problems found during solving. + problems: Vec<Problem>, + /// Learned rule pool: for each learned rule, the chain of rules that led to it. + learned_pool: Vec<Vec<RuleId>>, + /// Map from rule ID → learned pool index. + learned_why: IndexMap<RuleId, usize>, +} + +impl<'a> Solver<'a> { + /// Create a new solver with the given rules, pool, policy, and fixed package set. + pub fn new( + rules: RuleSet, + pool: &'a Pool, + policy: DefaultPolicy, + fixed_packages: IndexSet<PackageId>, + ) -> Self { + Solver { + pool, + policy, + rules, + watch_graph: RuleWatchGraph::new(), + decisions: Decisions::new(), + fixed_map: fixed_packages, + propagate_index: 0, + branches: Vec::new(), + problems: Vec::new(), + learned_pool: Vec::new(), + learned_why: IndexMap::new(), + } + } + + /// Solve the dependency resolution problem. + /// Returns the set of packages to install, or an error. + pub fn solve(mut self) -> Result<SolverResult, SolverError> { + // Insert all rules into watch graph + let rule_count = self.rules.len(); + for id in 0..rule_count { + let rule = self.rules.rule_by_id(id); + self.watch_graph.insert(id, rule); + } + + // Make decisions based on assertion rules (unit clauses) + self.make_assertion_rule_decisions()?; + + // Run the main SAT loop + self.run_sat()?; + + if !self.problems.is_empty() { + let messages: Vec<String> = self + .problems + .iter() + .map(|p| p.pretty_string(self.pool, &self.rules)) + .collect(); + return Err(SolverError::Unsolvable(messages)); + } + + // Collect installed packages + let mut installed = Vec::new(); + for i in 0..self.decisions.len() { + let decision = self.decisions.at_offset(i); + if decision.literal > 0 { + installed.push(literal_to_package_id(decision.literal)); + } + } + + Ok(SolverResult { installed }) + } + + /// Process assertion rules (unit clauses) — make immediate decisions. + /// + /// Port of Composer's Solver::makeAssertionRuleDecisions. + fn make_assertion_rule_decisions(&mut self) -> Result<(), SolverError> { + let decision_start = if self.decisions.is_empty() { + 0 + } else { + self.decisions.len() - 1 + }; + + let mut rule_index: usize = 0; + while rule_index < self.rules.len() { + let rule = self.rules.rule_by_id(rule_index); + + if !rule.is_assertion() || rule.is_disabled() { + rule_index += 1; + continue; + } + + let literal = rule.literals()[0]; + + if !self.decisions.decided(literal) { + self.decisions.decide(literal, 1, rule_index)?; + rule_index += 1; + continue; + } + + if self.decisions.satisfy(literal) { + rule_index += 1; + continue; + } + + // Found a conflict + let rule_type = self.rules.rule_by_id(rule_index).rule_type; + + if rule_type == RuleType::Learned { + self.rules.rule_by_id_mut(rule_index).disable(); + rule_index += 1; + continue; + } + + let conflict_rule_id = self.decisions.decision_rule(literal)?; + let conflict_type = self.rules.rule_by_id(conflict_rule_id).rule_type; + + if conflict_type == RuleType::Package { + let mut problem = Problem::new(); + problem.add_rule(rule_index); + problem.add_rule(conflict_rule_id); + self.rules.rule_by_id_mut(rule_index).disable(); + self.problems.push(problem); + rule_index += 1; + continue; + } + + // Conflict with another root require/fixed package + let mut problem = Problem::new(); + problem.add_rule(rule_index); + problem.add_rule(conflict_rule_id); + + // Push all request assertion rules asserting this literal + let pkg_id = literal_to_package_id(literal); + let request_rule_ids: Vec<RuleId> = self + .rules + .iter_type(RuleType::Request) + .filter(|(_, r)| { + !r.is_disabled() + && r.is_assertion() + && literal_to_package_id(r.literals()[0]) == pkg_id + }) + .map(|(id, _)| id) + .collect(); + + for rid in &request_rule_ids { + problem.add_rule(*rid); + } + self.problems.push(problem); + + for rid in request_rule_ids { + self.rules.rule_by_id_mut(rid).disable(); + } + + self.decisions.reset_to_offset(decision_start); + rule_index = 0; // restart + } + + Ok(()) + } + + /// Unit propagation: propagate decisions through the watch graph. + /// + /// Port of Composer's Solver::propagate. + fn propagate(&mut self, level: i32) -> Result<Option<RuleId>, SolverBugError> { + while self.decisions.valid_offset(self.propagate_index) { + let decision = self.decisions.at_offset(self.propagate_index).clone(); + self.propagate_index += 1; + + let conflict = self.watch_graph.propagate_literal( + decision.literal, + level, + &mut self.decisions, + &self.rules, + )?; + + if conflict.is_some() { + return Ok(conflict); + } + } + + Ok(None) + } + + /// Revert decisions to a given level. + /// + /// Port of Composer's Solver::revert. + fn revert(&mut self, level: i32) { + while !self.decisions.is_empty() { + let literal = self.decisions.last_literal(); + if self.decisions.undecided(literal) { + break; + } + let decision_level = self.decisions.decision_level(literal); + if decision_level <= level { + break; + } + self.decisions.revert_last(); + self.propagate_index = self.decisions.len(); + } + + while !self.branches.is_empty() && self.branches.last().unwrap().1 >= level { + self.branches.pop(); + } + } + + /// Make a decision, propagate, and learn from conflicts. + /// + /// Port of Composer's Solver::setPropagateLearn. + fn set_propagate_learn( + &mut self, + mut level: i32, + literal: Literal, + rule_id: RuleId, + ) -> Result<i32, SolverError> { + level += 1; + self.decisions.decide(literal, level, rule_id)?; + + loop { + let conflict = self.propagate(level)?; + + let Some(conflict_rule_id) = conflict else { + break; + }; + + if level == 1 { + self.analyze_unsolvable(conflict_rule_id); + return Ok(0); + } + + // Conflict analysis + let (learn_literal, new_level, new_rule, why) = + self.analyze(level, conflict_rule_id)?; + + if new_level <= 0 || new_level >= level { + return Err(SolverBugError { + message: format!( + "Trying to revert to invalid level {new_level} from level {level}." + ), + } + .into()); + } + + level = new_level; + self.revert(level); + + // Add learned rule + self.rules.add(new_rule, RuleType::Learned); + let new_rule_id = self.rules.len() - 1; + + self.learned_why.insert(new_rule_id, why); + + let rule_ref = self.rules.rule_by_id(new_rule_id); + self.watch_graph.insert(new_rule_id, rule_ref); + + // Adjust watch2 to highest level literal + let last_node = self.watch_graph.last_node_idx(); + let rule_for_watch = self.rules.rule_by_id(new_rule_id); + self.watch_graph + .watch2_on_highest(last_node, rule_for_watch, &self.decisions); + + self.decisions.decide(learn_literal, level, new_rule_id)?; + } + + Ok(level) + } + + /// Choose best package from candidates and install. + /// + /// Port of Composer's Solver::selectAndInstall. + fn select_and_install( + &mut self, + level: i32, + decision_queue: Vec<Literal>, + rule_id: RuleId, + ) -> Result<i32, SolverError> { + let required_package = self + .rules + .rule_by_id(rule_id) + .required_package() + .map(|s| s.to_string()); + let mut literals = self.policy.select_preferred_packages( + self.pool, + &decision_queue, + required_package.as_deref(), + ); + + let selected = literals.remove(0); + + // If there are remaining alternatives, save as branch point + if !literals.is_empty() { + self.branches.push((literals, level)); + } + + self.set_propagate_learn(level, selected, rule_id) + } + + /// First UIP conflict analysis. + /// + /// Port of Composer's Solver::analyze. + fn analyze( + &mut self, + level: i32, + conflict_rule_id: RuleId, + ) -> Result<(Literal, i32, Rule, usize), SolverError> { + let mut rule_level: i32 = 1; + let mut num: i32 = 0; + let mut l1num: i32 = 0; + let mut seen: IndexSet<PackageId> = IndexSet::new(); + let mut learned_literal: Option<Literal> = None; + let mut other_learned_literals: Vec<Literal> = Vec::new(); + + let mut decision_id = self.decisions.len(); + + self.learned_pool.push(Vec::new()); + let pool_idx = self.learned_pool.len() - 1; + + let mut current_rule_id = conflict_rule_id; + + loop { + self.learned_pool[pool_idx].push(current_rule_id); + + let rule = self.rules.rule_by_id(current_rule_id); + let rule_literals = rule.literals().to_vec(); + let is_multi_conflict = rule.is_multi_conflict; + + for &literal in &rule_literals { + // MultiConflictRule: skip undecided literals + if is_multi_conflict && !self.decisions.decided(literal) { + continue; + } + + // Skip the one true literal + if self.decisions.satisfy(literal) { + continue; + } + + let pkg_id = literal_to_package_id(literal); + if seen.contains(&pkg_id) { + continue; + } + seen.insert(pkg_id); + + let l = self.decisions.decision_level(literal); + + if l == 1 { + l1num += 1; + } else if l == level { + num += 1; + } else { + other_learned_literals.push(literal); + if l > rule_level { + rule_level = l; + } + } + } + + // l1 retry loop + let mut l1retry = true; + while l1retry { + l1retry = false; + + if num == 0 { + l1num -= 1; + if l1num == 0 { + // All level 1 literals done + let why = pool_idx; + let ll = learned_literal.ok_or_else(|| SolverBugError { + message: format!( + "Did not find a learnable literal in analyzed rule {conflict_rule_id}." + ), + })?; + + let mut all_literals = vec![ll]; + all_literals.extend_from_slice(&other_learned_literals); + + let new_rule = + Rule::new(all_literals, RuleReason::Learned, ReasonData::Learned(why)); + + return Ok((ll, rule_level, new_rule, why)); + } + } + + loop { + if decision_id == 0 { + return Err(SolverBugError { + message: format!( + "Reached invalid decision id 0 while analyzing rule {conflict_rule_id}." + ), + } + .into()); + } + + decision_id -= 1; + let decision = self.decisions.at_offset(decision_id); + let literal = decision.literal; + + if seen.contains(&literal_to_package_id(literal)) { + break; + } + } + + let decision = self.decisions.at_offset(decision_id); + let literal = decision.literal; + + seen.shift_remove(&literal_to_package_id(literal)); + + if num != 0 { + num -= 1; + if num == 0 { + learned_literal = Some(-literal); + + if l1num == 0 { + // Done + let why = pool_idx; + let ll = learned_literal.unwrap(); + + let mut all_literals = vec![ll]; + all_literals.extend_from_slice(&other_learned_literals); + + let new_rule = Rule::new( + all_literals, + RuleReason::Learned, + ReasonData::Learned(why), + ); + + return Ok((ll, rule_level, new_rule, why)); + } + + // Only level 1 marks left + for other in &other_learned_literals { + seen.shift_remove(&literal_to_package_id(*other)); + } + l1num += 1; + l1retry = true; + } else { + let decision = self.decisions.at_offset(decision_id); + let next_rule_id = decision.rule_id; + let next_rule = self.rules.rule_by_id(next_rule_id); + + if next_rule.is_multi_conflict { + // Handle multi-conflict rule + let mcr_literals = next_rule.literals().to_vec(); + for &rule_literal in &mcr_literals { + let pkg_id = literal_to_package_id(rule_literal); + if !seen.contains(&pkg_id) && self.decisions.satisfy(-rule_literal) + { + self.learned_pool[pool_idx].push(next_rule_id); + let l = self.decisions.decision_level(rule_literal); + if l == 1 { + l1num += 1; + } else if l == level { + num += 1; + } else { + other_learned_literals.push(rule_literal); + if l > rule_level { + rule_level = l; + } + } + seen.insert(pkg_id); + break; + } + } + l1retry = true; + } + } + } + } + + let decision = self.decisions.at_offset(decision_id); + current_rule_id = decision.rule_id; + } + } + + /// Recursively collect rules involved in an unsolvable conflict. + fn analyze_unsolvable_rule( + &self, + problem: &mut Problem, + conflict_rule_id: RuleId, + rule_seen: &mut IndexSet<RuleId>, + ) { + if rule_seen.contains(&conflict_rule_id) { + return; + } + rule_seen.insert(conflict_rule_id); + + let rule = self.rules.rule_by_id(conflict_rule_id); + + if rule.rule_type == RuleType::Learned { + if let Some(&why) = self.learned_why.get(&conflict_rule_id) + && let Some(problem_rules) = self.learned_pool.get(why) + { + for &pr_id in problem_rules { + if !rule_seen.contains(&pr_id) { + self.analyze_unsolvable_rule(problem, pr_id, rule_seen); + } + } + } + return; + } + + if rule.rule_type == RuleType::Package { + // Package rules cannot be part of a problem + return; + } + + problem.next_section(); + problem.add_rule(conflict_rule_id); + } + + /// Analyze an unsolvable conflict at level 1. + /// + /// Port of Composer's Solver::analyzeUnsolvable. + fn analyze_unsolvable(&mut self, conflict_rule_id: RuleId) { + let mut problem = Problem::new(); + problem.add_rule(conflict_rule_id); + + let mut rule_seen = IndexSet::new(); + self.analyze_unsolvable_rule(&mut problem, conflict_rule_id, &mut rule_seen); + + // Collect related decisions + let mut seen: IndexSet<PackageId> = IndexSet::new(); + let conflict_literals = self.rules.rule_by_id(conflict_rule_id).literals().to_vec(); + for &lit in &conflict_literals { + if self.decisions.satisfy(lit) { + continue; + } + seen.insert(literal_to_package_id(lit)); + } + + // Walk decisions in reverse + for i in (0..self.decisions.len()).rev() { + let decision = self.decisions.at_offset(i); + let dec_literal = decision.literal; + let pkg_id = literal_to_package_id(dec_literal); + + if !seen.contains(&pkg_id) { + continue; + } + + let why = decision.rule_id; + problem.add_rule(why); + self.analyze_unsolvable_rule(&mut problem, why, &mut rule_seen); + + let why_literals = self.rules.rule_by_id(why).literals().to_vec(); + for &lit in &why_literals { + if self.decisions.satisfy(lit) { + continue; + } + seen.insert(literal_to_package_id(lit)); + } + } + + self.problems.push(problem); + } + + /// Main SAT loop. + /// + /// Port of Composer's Solver::runSat. + fn run_sat(&mut self) -> Result<(), SolverError> { + self.propagate_index = 0; + + let mut level: i32 = 1; + let mut system_level: i32 = level + 1; + + loop { + // Step 1: propagate at level 1 + if level == 1 { + let conflict = self.propagate(level)?; + if let Some(conflict_rule_id) = conflict { + self.analyze_unsolvable(conflict_rule_id); + return Ok(()); + } + } + + // Step 2: handle root require/fixed package rules + if level < system_level { + let mut made_decision = false; + + // Collect request rule IDs first to avoid borrow issues + let request_rule_ids: Vec<RuleId> = self + .rules + .iter_type(RuleType::Request) + .map(|(id, _)| id) + .collect(); + + let mut all_satisfied = true; + + for &rule_id in &request_rule_ids { + let rule = self.rules.rule_by_id(rule_id); + if !rule.is_enabled() { + continue; + } + + let mut decision_queue: Vec<Literal> = Vec::new(); + let mut none_satisfied = true; + + for &lit in rule.literals() { + if self.decisions.satisfy(lit) { + none_satisfied = false; + break; + } + if lit > 0 && self.decisions.undecided(lit) { + decision_queue.push(lit); + } + } + + if none_satisfied && !decision_queue.is_empty() { + // Prune: prefer fixed packages + let pruned: Vec<Literal> = decision_queue + .iter() + .filter(|&&lit| self.fixed_map.contains(&literal_to_package_id(lit))) + .copied() + .collect(); + + if !pruned.is_empty() { + decision_queue = pruned; + } + } + + if none_satisfied && !decision_queue.is_empty() { + let old_level = level; + level = self.select_and_install(level, decision_queue, rule_id)?; + + if level == 0 { + return Ok(()); + } + if level <= old_level { + made_decision = true; + break; + } + } + + // Check if there are more rules to process + all_satisfied = false; + } + + system_level = level + 1; + + if made_decision || !all_satisfied { + // Check if we still have unsatisfied request rules + let has_unsatisfied = request_rule_ids.iter().any(|&rule_id| { + let rule = self.rules.rule_by_id(rule_id); + if !rule.is_enabled() { + return false; + } + let mut none_satisfied = true; + for &lit in rule.literals() { + if self.decisions.satisfy(lit) { + none_satisfied = false; + break; + } + } + if !none_satisfied { + return false; + } + rule.literals() + .iter() + .any(|&lit| lit > 0 && self.decisions.undecided(lit)) + }); + + if has_unsatisfied { + continue; + } + } + } + + if level < system_level { + system_level = level; + } + + // Step 3: fulfill all unresolved rules + let mut rules_count = self.rules.len(); + let mut i: usize = 0; + let mut n: usize = 0; + let mut made_decision = false; + + while n < rules_count { + if i == rules_count { + i = 0; + } + + let rule = self.rules.rule_by_id(i); + let literals = rule.literals().to_vec(); + + i += 1; + n += 1; + + if rule.is_disabled() { + continue; + } + + let mut decision_queue: Vec<Literal> = Vec::new(); + let mut skip = false; + + for &lit in &literals { + if lit <= 0 { + if !self.decisions.decided_install(lit) { + skip = true; + break; + } + } else { + if self.decisions.decided_install(lit) { + skip = true; + break; + } + if self.decisions.undecided(lit) { + decision_queue.push(lit); + } + } + } + + if skip { + continue; + } + + // Need at least 2 undecided positive literals + if decision_queue.len() < 2 { + continue; + } + + let rule_id = i - 1; + level = self.select_and_install(level, decision_queue, rule_id)?; + + if level == 0 { + return Ok(()); + } + + // Something changed, restart scan + rules_count = self.rules.len(); + n = 0; + i = 0; + made_decision = true; + } + + if level < system_level && made_decision { + continue; + } + + // Step 4: minimization (backjumping) + if !self.branches.is_empty() { + let mut last_literal: Option<Literal> = None; + let mut last_level: Option<i32> = None; + let mut last_branch_index: usize = 0; + let mut last_branch_offset: usize = 0; + + for i in (0..self.branches.len()).rev() { + let (ref literals, l) = self.branches[i]; + for (offset, &literal) in literals.iter().enumerate() { + if literal > 0 && self.decisions.decision_level(literal) > l + 1 { + last_literal = Some(literal); + last_branch_index = i; + last_branch_offset = offset; + last_level = Some(l); + } + } + } + + if let Some(literal) = last_literal { + let last_l = last_level.unwrap(); + self.branches[last_branch_index] + .0 + .remove(last_branch_offset); + + level = last_l; + self.revert(level); + + let why = self.decisions.last_reason(); + + level = self.set_propagate_learn(level, literal, why)?; + + if level == 0 { + return Ok(()); + } + + continue; + } + } + + break; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dependency_resolver::pool::PoolPackageInput; + use crate::dependency_resolver::rule::{ReasonData, Rule, RuleReason, RuleType}; + + fn make_input(name: &str, version: &str) -> PoolPackageInput { + PoolPackageInput { + name: name.to_string(), + version: version.to_string(), + pretty_version: version.to_string(), + requires: vec![], + replaces: vec![], + provides: vec![], + conflicts: vec![], + is_fixed: false, + is_alias_of: None, + } + } + + /// Helper: create a simple problem and solve it. + /// Creates a pool with N dummy packages (1..=max_id). + fn make_rules_and_solve( + rules: Vec<(Rule, RuleType)>, + fixed: IndexSet<PackageId>, + max_id: u32, + ) -> Result<SolverResult, SolverError> { + let mut rs = RuleSet::new(); + for (rule, rt) in rules { + rs.add(rule, rt); + } + let inputs: Vec<_> = (1..=max_id) + .map(|i| make_input(&format!("pkg/{i}"), &format!("{i}.0.0.0"))) + .collect(); + let pool = Pool::new(inputs, vec![]); + let policy = DefaultPolicy::default(); + let solver = Solver::new(rs, &pool, policy, fixed); + solver.solve() + } + + #[test] + fn test_single_package_required() { + // Root requires package 1 + let result = make_rules_and_solve( + vec![( + Rule::new(vec![1], RuleReason::RootRequire, ReasonData::None), + RuleType::Request, + )], + IndexSet::new(), + 3, + ) + .unwrap(); + + assert_eq!(result.installed, vec![1]); + } + + #[test] + fn test_two_packages_required() { + // Root requires either package 1 or 2, and also requires 3 + let result = make_rules_and_solve( + vec![ + ( + Rule::new(vec![1, 2], RuleReason::RootRequire, ReasonData::None), + RuleType::Request, + ), + ( + Rule::new(vec![3], RuleReason::RootRequire, ReasonData::None), + RuleType::Request, + ), + ], + IndexSet::new(), + 3, + ) + .unwrap(); + + assert!(result.installed.contains(&3)); + // Should install one of 1 or 2 + assert!(result.installed.contains(&1) || result.installed.contains(&2)); + } + + #[test] + fn test_dependency_chain() { + // Root requires 1. Package 1 requires 2. + // Rule for root: (1) + // Rule for dep: (-1 | 2) + let result = make_rules_and_solve( + vec![ + ( + Rule::new(vec![1], RuleReason::RootRequire, ReasonData::None), + RuleType::Request, + ), + ( + Rule::new(vec![-1, 2], RuleReason::PackageRequires, ReasonData::None), + RuleType::Package, + ), + ], + IndexSet::new(), + 3, + ) + .unwrap(); + + assert!(result.installed.contains(&1)); + assert!(result.installed.contains(&2)); + } + + #[test] + fn test_conflict_resolution() { + // Root requires 1 or 2. Package 1 conflicts with 3. + // Package 2 requires 3. + // Rules: + // Request: (1 | 2) + // Package: (-1 | -3) -- conflict + // Package: (-2 | 3) -- dep + // Request: (3) -- root also requires 3 + let result = make_rules_and_solve( + vec![ + ( + Rule::new(vec![1, 2], RuleReason::RootRequire, ReasonData::None), + RuleType::Request, + ), + ( + Rule::two_literals(-1, -3, RuleReason::PackageConflict, ReasonData::None), + RuleType::Package, + ), + ( + Rule::new(vec![-2, 3], RuleReason::PackageRequires, ReasonData::None), + RuleType::Package, + ), + ( + Rule::new(vec![3], RuleReason::RootRequire, ReasonData::None), + RuleType::Request, + ), + ], + IndexSet::new(), + 3, + ) + .unwrap(); + + // Package 3 is required, so 1 conflicts, must choose 2 + assert!(result.installed.contains(&2)); + assert!(result.installed.contains(&3)); + assert!(!result.installed.contains(&1)); + } + + #[test] + fn test_same_name_conflict() { + // Two versions of same package: 1 and 2. Root requires either. + // Same-name rule: (-1 | -2) + let result = make_rules_and_solve( + vec![ + ( + Rule::new(vec![1, 2], RuleReason::RootRequire, ReasonData::None), + RuleType::Request, + ), + ( + Rule::two_literals(-1, -2, RuleReason::PackageSameName, ReasonData::None), + RuleType::Package, + ), + ], + IndexSet::new(), + 3, + ) + .unwrap(); + + // Should install exactly one + let has_1 = result.installed.contains(&1); + let has_2 = result.installed.contains(&2); + assert!(has_1 ^ has_2, "Should install exactly one of 1 or 2"); + } + + #[test] + fn test_unsolvable() { + // Root requires 1. Root requires 2. But 1 and 2 conflict. + let result = make_rules_and_solve( + vec![ + ( + Rule::new(vec![1], RuleReason::RootRequire, ReasonData::None), + RuleType::Request, + ), + ( + Rule::new(vec![2], RuleReason::RootRequire, ReasonData::None), + RuleType::Request, + ), + ( + Rule::two_literals(-1, -2, RuleReason::PackageConflict, ReasonData::None), + RuleType::Package, + ), + ], + IndexSet::new(), + 3, + ); + + assert!(result.is_err()); + } +} |
