use std::borrow::Borrow; use std::str::FromStr; use itertools::Itertools; use rustc_hash::FxHashMap; use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pep508::{ ExtraOperator, MarkerEnvironment, MarkerEnvironmentBuilder, MarkerExpression, MarkerOperator, MarkerTree, }; use uv_pypi_types::{ConflictItem, ConflictPackage, Conflicts}; use crate::ResolveError; /// A representation of a marker for use in universal resolution. /// /// (This degrades gracefully to a standard PEP 508 marker in the case of /// non-universal resolution.) /// /// This universal marker is meant to combine both a PEP 508 marker and a /// marker for conflicting extras/groups. The latter specifically expresses /// whether a particular edge in a dependency graph should be followed /// depending on the activated extras and groups. /// /// A universal marker evaluates to true only when *both* its PEP 508 marker /// and its conflict marker evaluate to true. #[derive(Default, Copy, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)] pub struct UniversalMarker { /// The full combined PEP 508 and "conflict" marker. /// /// In the original design, the PEP 508 marker was kept separate /// from the conflict marker, since the conflict marker is not really /// specified by PEP 508. However, this approach turned out to be /// bunk because the conflict marker vary depending on which part of /// the PEP 508 marker is true. For example, you might have a different /// conflict marker for one platform versus the other. The only way to /// resolve this is to combine them both into one marker. /// /// The downside of this is that since conflict markers aren't part of /// PEP 508, combining them is pretty weird. We could combine them into /// a new type of marker that isn't PEP 508. But it's not clear what the /// best design for that is, and at the time of writing, it would have /// been a lot of additional work. (Our PEP 508 marker implementation is /// rather sophisticated given its boolean simplification capabilities. /// So leveraging all that work is a huge shortcut.) So to accomplish /// this, we technically preserve PEP 508 compatibility but abuse the /// `extra` attribute to encode conflicts. /// /// So for example, if a particular dependency should only be activated /// on `Darwin` and when the extra `x1` for package `foo` is enabled, /// then its "universal" marker looks like this: /// /// ```text /// sys_platform == 'Darwin' and extra == 'extra-3-foo-x1' /// ``` /// /// Then, when `uv sync --extra x1` is called, we encode that was /// `extra-3-foo-x1` and pass it as-needed when evaluating this marker. /// /// Why `extra-3-foo-x1`? /// /// * The `extra` prefix is there to distinguish it from `group`. /// * The `3` is there to indicate the length of the package name, /// in bytes. This isn't strictly necessary for encoding, but /// is required if we were ever to need to decode a package and /// extra/group name from a conflict marker. /// * The `foo` package name ensures we namespace the extra/group name, /// since multiple packages can have the same extra/group name. /// /// We only use alphanumeric characters and hyphens in order to limit /// ourselves to valid extra names. (If we could use other characters then /// that would avoid the need to encode the length of the package name.) /// /// So while the above marker is still technically valid from a PEP 508 /// stand-point, evaluating it requires uv's custom encoding of extras (and /// groups). marker: MarkerTree, /// The strictly PEP 508 version of `marker`. Basically, `marker`, but /// without any extras in it. This could be computed on demand (and /// that's what we used to do), but we do it enough that it was causing a /// regression in some cases. pep508: MarkerTree, } impl UniversalMarker { /// A constant universal marker that always evaluates to `true`. pub(crate) const TRUE: UniversalMarker = UniversalMarker { marker: MarkerTree::TRUE, pep508: MarkerTree::TRUE, }; /// A constant universal marker that always evaluates to `false`. pub(crate) const FALSE: UniversalMarker = UniversalMarker { marker: MarkerTree::FALSE, pep508: MarkerTree::FALSE, }; /// Creates a new universal marker from its constituent pieces. pub(crate) fn new( mut pep508_marker: MarkerTree, conflict_marker: ConflictMarker, ) -> UniversalMarker { pep508_marker.and(conflict_marker.marker); UniversalMarker::from_combined(pep508_marker) } /// Creates a new universal marker from a marker that has already been /// combined from a PEP 508 and conflict marker. pub(crate) fn from_combined(marker: MarkerTree) -> UniversalMarker { UniversalMarker { marker, pep508: marker.without_extras(), } } /// Combine this universal marker with the one given in a way that unions /// them. That is, the updated marker will evaluate to `true` if `self` or /// `other` evaluate to `true`. pub(crate) fn or(&mut self, other: UniversalMarker) { self.marker.or(other.marker); self.pep508.or(other.pep508); } /// Combine this universal marker with the one given in a way that /// intersects them. That is, the updated marker will evaluate to `true` if /// `self` and `other` evaluate to `true`. pub(crate) fn and(&mut self, other: UniversalMarker) { self.marker.and(other.marker); self.pep508.and(other.pep508); } /// Imbibes the world knowledge expressed by `conflicts` into this marker. /// /// This will effectively simplify the conflict marker in this universal /// marker. In particular, it enables simplifying based on the fact that no /// two items from the same set in the given conflicts can be active at a /// given time. pub(crate) fn imbibe(&mut self, conflicts: ConflictMarker) { let self_marker = self.marker; self.marker = conflicts.marker; self.marker.implies(self_marker); self.pep508 = self.marker.without_extras(); } /// Assumes that a given extra/group for the given package is activated. /// /// This may simplify the conflicting marker component of this universal /// marker. pub(crate) fn assume_conflict_item(&mut self, item: &ConflictItem) { match *item.conflict() { ConflictPackage::Extra(ref extra) => self.assume_extra(item.package(), extra), ConflictPackage::Group(ref group) => self.assume_group(item.package(), group), } self.pep508 = self.marker.without_extras(); } /// Assumes that a given extra/group for the given package is not /// activated. /// /// This may simplify the conflicting marker component of this universal /// marker. pub(crate) fn assume_not_conflict_item(&mut self, item: &ConflictItem) { match *item.conflict() { ConflictPackage::Extra(ref extra) => self.assume_not_extra(item.package(), extra), ConflictPackage::Group(ref group) => self.assume_not_group(item.package(), group), } self.pep508 = self.marker.without_extras(); } /// Assumes that a given extra for the given package is activated. /// /// This may simplify the conflicting marker component of this universal /// marker. pub(crate) fn assume_extra(&mut self, package: &PackageName, extra: &ExtraName) { let extra = encode_package_extra(package, extra); self.marker = self .marker .simplify_extras_with(|candidate| *candidate == extra); self.pep508 = self.marker.without_extras(); } /// Assumes that a given extra for the given package is not activated. /// /// This may simplify the conflicting marker component of this universal /// marker. pub(crate) fn assume_not_extra(&mut self, package: &PackageName, extra: &ExtraName) { let extra = encode_package_extra(package, extra); self.marker = self .marker .simplify_not_extras_with(|candidate| *candidate == extra); self.pep508 = self.marker.without_extras(); } /// Assumes that a given group for the given package is activated. /// /// This may simplify the conflicting marker component of this universal /// marker. pub(crate) fn assume_group(&mut self, package: &PackageName, group: &GroupName) { let extra = encode_package_group(package, group); self.marker = self .marker .simplify_extras_with(|candidate| *candidate == extra); self.pep508 = self.marker.without_extras(); } /// Assumes that a given group for the given package is not activated. /// /// This may simplify the conflicting marker component of this universal /// marker. pub(crate) fn assume_not_group(&mut self, package: &PackageName, group: &GroupName) { let extra = encode_package_group(package, group); self.marker = self .marker .simplify_not_extras_with(|candidate| *candidate == extra); self.pep508 = self.marker.without_extras(); } /// Returns true if this universal marker will always evaluate to `true`. pub(crate) fn is_true(self) -> bool { self.marker.is_true() } /// Returns true if this universal marker will always evaluate to `false`. pub(crate) fn is_false(self) -> bool { self.marker.is_false() } /// Returns true if this universal marker is disjoint with the one given. /// /// Two universal markers are disjoint when it is impossible for them both /// to evaluate to `true` simultaneously. pub(crate) fn is_disjoint(self, other: UniversalMarker) -> bool { self.marker.is_disjoint(other.marker) } /// Returns true if this universal marker is satisfied by the given marker /// environment. /// /// This should only be used when evaluating a marker that is known not to /// have any extras. For example, the PEP 508 markers on a fork. pub(crate) fn evaluate_no_extras(self, env: &MarkerEnvironment) -> bool { self.marker.evaluate(env, &[]) } /// Returns true if this universal marker is satisfied by the given marker /// environment and list of activated extras and groups. /// /// The activated extras and groups should be the complete set activated /// for a particular context. And each extra and group must be scoped to /// the particular package that it's enabled for. pub(crate) fn evaluate( self, env: &MarkerEnvironment, extras: impl Iterator, groups: impl Iterator, ) -> bool where P: Borrow, E: Borrow, G: Borrow, { let extras = extras.map(|(package, extra)| encode_package_extra(package.borrow(), extra.borrow())); let groups = groups.map(|(package, group)| encode_package_group(package.borrow(), group.borrow())); self.marker .evaluate(env, &extras.chain(groups).collect::>()) } /// Returns the internal marker that combines both the PEP 508 /// and conflict marker. pub fn combined(self) -> MarkerTree { self.marker } /// Returns the PEP 508 marker for this universal marker. /// /// One should be cautious using this. Generally speaking, it should only /// be used when one knows universal resolution isn't in effect. When /// universal resolution is enabled (i.e., there may be multiple forks /// producing different versions of the same package), then one should /// always use a universal marker since it accounts for all possible ways /// for a package to be installed. pub fn pep508(self) -> MarkerTree { self.pep508 } /// Returns the non-PEP 508 marker expression that represents conflicting /// extras/groups. /// /// Like with `UniversalMarker::pep508`, one should be cautious when using /// this. It is generally always wrong to consider conflicts in isolation /// from PEP 508 markers. But this can be useful for detecting failure /// cases. For example, the code for emitting a `ResolverOutput` (even a /// universal one) in a `requirements.txt` format checks for the existence /// of non-trivial conflict markers and fails if any are found. (Because /// conflict markers cannot be represented in the `requirements.txt` /// format.) pub(crate) fn conflict(self) -> ConflictMarker { ConflictMarker { marker: self.marker.only_extras(), } } } impl std::fmt::Debug for UniversalMarker { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { std::fmt::Debug::fmt(&self.marker, f) } } /// A marker that is only for representing conflicting extras/groups. /// /// This encapsulates the encoding of extras and groups into PEP 508 /// markers. #[derive(Default, Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)] pub struct ConflictMarker { marker: MarkerTree, } impl ConflictMarker { /// A constant conflict marker that always evaluates to `true`. pub const TRUE: ConflictMarker = ConflictMarker { marker: MarkerTree::TRUE, }; /// A constant conflict marker that always evaluates to `false`. pub const FALSE: ConflictMarker = ConflictMarker { marker: MarkerTree::FALSE, }; /// Creates a new conflict marker from the declared conflicts provided. pub fn from_conflicts(conflicts: &Conflicts) -> ConflictMarker { if conflicts.is_empty() { return ConflictMarker::TRUE; } let mut marker = ConflictMarker::TRUE; for set in conflicts.iter() { for (item1, item2) in set.iter().tuple_combinations() { let pair = ConflictMarker::from_conflict_item(item1) .negate() .or(ConflictMarker::from_conflict_item(item2).negate()); marker = marker.and(pair); } } marker } /// Create a conflict marker that is true only when the given extra or /// group (for a specific package) is activated. pub fn from_conflict_item(item: &ConflictItem) -> ConflictMarker { match *item.conflict() { ConflictPackage::Extra(ref extra) => ConflictMarker::extra(item.package(), extra), ConflictPackage::Group(ref group) => ConflictMarker::group(item.package(), group), } } /// Create a conflict marker that is true only when the given extra for the /// given package is activated. pub fn extra(package: &PackageName, extra: &ExtraName) -> ConflictMarker { let operator = uv_pep508::ExtraOperator::Equal; let name = uv_pep508::MarkerValueExtra::Extra(encode_package_extra(package, extra)); let expr = uv_pep508::MarkerExpression::Extra { operator, name }; let marker = MarkerTree::expression(expr); ConflictMarker { marker } } /// Create a conflict marker that is true only when the given group for the /// given package is activated. pub fn group(package: &PackageName, group: &GroupName) -> ConflictMarker { let operator = uv_pep508::ExtraOperator::Equal; let name = uv_pep508::MarkerValueExtra::Extra(encode_package_group(package, group)); let expr = uv_pep508::MarkerExpression::Extra { operator, name }; let marker = MarkerTree::expression(expr); ConflictMarker { marker } } /// Returns a new conflict marker that is the negation of this one. #[must_use] pub fn negate(self) -> ConflictMarker { ConflictMarker { marker: self.marker.negate(), } } /// Returns a new conflict marker corresponding to the union of `self` and /// `other`. #[must_use] pub fn or(self, other: ConflictMarker) -> ConflictMarker { let mut marker = self.marker; marker.or(other.marker); ConflictMarker { marker } } /// Returns a new conflict marker corresponding to the intersection of /// `self` and `other`. #[must_use] pub fn and(self, other: ConflictMarker) -> ConflictMarker { let mut marker = self.marker; marker.and(other.marker); ConflictMarker { marker } } /// Returns a new conflict marker corresponding to the logical implication /// of `self` and the given consequent. /// /// If the conflict marker returned is always `true`, then it can be said /// that `self` implies `consequent`. #[must_use] pub fn implies(self, other: ConflictMarker) -> ConflictMarker { let mut marker = self.marker; marker.implies(other.marker); ConflictMarker { marker } } /// Returns true if this conflict marker will always evaluate to `true`. pub fn is_true(self) -> bool { self.marker.is_true() } /// Returns true if this conflict marker will always evaluate to `false`. pub fn is_false(self) -> bool { self.marker.is_false() } /// Returns true if this conflict marker is satisfied by the given /// list of activated extras and groups. pub(crate) fn evaluate(self, extras: &[(P, E)], groups: &[(P, G)]) -> bool where P: Borrow, E: Borrow, G: Borrow, { static DUMMY: std::sync::LazyLock = std::sync::LazyLock::new(|| { MarkerEnvironment::try_from(MarkerEnvironmentBuilder { implementation_name: "", implementation_version: "3.7", os_name: "linux", platform_machine: "", platform_python_implementation: "", platform_release: "", platform_system: "", platform_version: "", python_full_version: "3.7", python_version: "3.7", sys_platform: "linux", }) .unwrap() }); let extras = extras .iter() .map(|(package, extra)| encode_package_extra(package.borrow(), extra.borrow())); let groups = groups .iter() .map(|(package, group)| encode_package_group(package.borrow(), group.borrow())); self.marker .evaluate(&DUMMY, &extras.chain(groups).collect::>()) } /// Returns inclusion and exclusion (respectively) conflict items parsed /// from this conflict marker. /// /// This returns an error if any `extra` could not be parsed as a valid /// encoded conflict extra. pub(crate) fn filter_rules( self, ) -> Result<(Vec, Vec), ResolveError> { let (mut raw_include, mut raw_exclude) = (vec![], vec![]); self.marker.visit_extras(|op, extra| { match op { MarkerOperator::Equal => raw_include.push(extra.to_owned()), MarkerOperator::NotEqual => raw_exclude.push(extra.to_owned()), // OK by the contract of `MarkerTree::visit_extras`. _ => unreachable!(), } }); let include = raw_include .into_iter() .map(|extra| ParsedRawExtra::parse(&extra).and_then(|parsed| parsed.to_conflict_item())) .collect::, _>>()?; let exclude = raw_exclude .into_iter() .map(|extra| ParsedRawExtra::parse(&extra).and_then(|parsed| parsed.to_conflict_item())) .collect::, _>>()?; Ok((include, exclude)) } } impl std::fmt::Debug for ConflictMarker { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { // This is a little more succinct than the default. write!(f, "ConflictMarker({:?})", self.marker) } } /// Encodes the given package name and its corresponding extra into a valid /// `extra` value in a PEP 508 marker. fn encode_package_extra(package: &PackageName, extra: &ExtraName) -> ExtraName { // This is OK because `PackageName` and `ExtraName` have the same // validation rules, and we combine them in a way that always results in a // valid name. // // Note also that we encode the length of the package name (in bytes) into // the encoded extra name as well. This ensures we can parse out both the // package and extra name if necessary. If we didn't do this, then some // cases could be ambiguous since our field delimiter (`-`) is also a valid // character in `package` or `extra` values. But if we know the length of // the package name, we can always parse each field unambiguously. let package_len = package.as_str().len(); ExtraName::from_owned(format!("extra-{package_len}-{package}-{extra}")).unwrap() } /// Encodes the given package name and its corresponding group into a valid /// `extra` value in a PEP 508 marker. fn encode_package_group(package: &PackageName, group: &GroupName) -> ExtraName { // See `encode_package_extra`, the same considerations apply here. let package_len = package.as_str().len(); ExtraName::from_owned(format!("group-{package_len}-{package}-{group}")).unwrap() } #[derive(Debug)] enum ParsedRawExtra<'a> { Extra { package: &'a str, extra: &'a str }, Group { package: &'a str, group: &'a str }, } impl<'a> ParsedRawExtra<'a> { fn parse(raw_extra: &'a ExtraName) -> Result, ResolveError> { fn mkerr(raw_extra: &ExtraName, reason: impl Into) -> ResolveError { let raw_extra = raw_extra.to_owned(); let reason = reason.into(); ResolveError::InvalidExtraInConflictMarker { reason, raw_extra } } let raw = raw_extra.as_str(); let Some((kind, tail)) = raw.split_once('-') else { return Err(mkerr( raw_extra, "expected to find leading `extra-` or `group-`", )); }; let Some((len, tail)) = tail.split_once('-') else { return Err(mkerr( raw_extra, "expected to find `{number}-` after leading `extra-` or `group-`", )); }; let len = len.parse::().map_err(|_| { mkerr( raw_extra, format!("found package length number `{len}`, but could not parse into integer"), ) })?; let Some((package, tail)) = tail.split_at_checked(len) else { return Err(mkerr( raw_extra, format!( "expected at least {len} bytes for package name, but found {found}", found = tail.len() ), )); }; if !tail.starts_with('-') { return Err(mkerr( raw_extra, format!("expected `-` after package name `{package}`"), )); } let tail = &tail[1..]; match kind { "extra" => Ok(ParsedRawExtra::Extra { package, extra: tail, }), "group" => Ok(ParsedRawExtra::Group { package, group: tail, }), _ => Err(mkerr( raw_extra, format!("unrecognized kind `{kind}` (must be `extra` or `group`)"), )), } } fn to_conflict_item(&self) -> Result { let package = PackageName::from_str(self.package()).map_err(|name_error| { ResolveError::InvalidValueInConflictMarker { kind: "package", name_error, } })?; match *self { ParsedRawExtra::Extra { extra, .. } => { let extra = ExtraName::from_str(extra).map_err(|name_error| { ResolveError::InvalidValueInConflictMarker { kind: "extra", name_error, } })?; Ok(ConflictItem::from((package, extra))) } ParsedRawExtra::Group { group, .. } => { let group = GroupName::from_str(group).map_err(|name_error| { ResolveError::InvalidValueInConflictMarker { kind: "group", name_error, } })?; Ok(ConflictItem::from((package, group))) } } } fn package(&self) -> &'a str { match *self { ParsedRawExtra::Extra { package, .. } => package, ParsedRawExtra::Group { package, .. } => package, } } } /// Resolve the conflict markers in a [`MarkerTree`] based on the conditions under which each /// conflict item is known to be true. /// /// For example, if the `cpu` extra is known to be enabled when `sys_platform == 'darwin'`, then /// given the combined marker `python_version >= '3.8' and extra == 'extra-7-project-cpu'`, this /// method would return `python_version >= '3.8' and sys_platform == 'darwin'`. /// /// If a conflict item isn't present in the map of known conflicts, it's assumed to be false in all /// environments. pub(crate) fn resolve_conflicts( marker: MarkerTree, known_conflicts: &FxHashMap, ) -> MarkerTree { if marker.is_true() || marker.is_false() { return marker; } let mut transformed = MarkerTree::FALSE; // Convert the marker to DNF, then re-build it. for dnf in marker.to_dnf() { let mut or = MarkerTree::TRUE; for marker in dnf { let MarkerExpression::Extra { ref operator, ref name, } = marker else { or.and(MarkerTree::expression(marker)); continue; }; let Some(name) = name.as_extra() else { or.and(MarkerTree::expression(marker)); continue; }; // Given an extra marker (like `extra == 'extra-7-project-cpu'`), search for the // corresponding conflict; once found, inline the marker of conditions under which the // conflict is known to be true. let mut found = false; for (conflict_item, conflict_marker) in known_conflicts { // Search for the conflict item as an extra. if let Some(extra) = conflict_item.extra() { let package = conflict_item.package(); let encoded = encode_package_extra(package, extra); if encoded == *name { match operator { ExtraOperator::Equal => { or.and(*conflict_marker); found = true; break; } ExtraOperator::NotEqual => { or.and(conflict_marker.negate()); found = true; break; } } } } // Search for the conflict item as a group. if let Some(group) = conflict_item.group() { let package = conflict_item.package(); let encoded = encode_package_group(package, group); if encoded == *name { match operator { ExtraOperator::Equal => { or.and(*conflict_marker); found = true; break; } ExtraOperator::NotEqual => { or.and(conflict_marker.negate()); found = true; break; } } } } } // If we didn't find the marker in the list of known conflicts, assume it's always // false. if !found { match operator { ExtraOperator::Equal => { or.and(MarkerTree::FALSE); } ExtraOperator::NotEqual => { or.and(MarkerTree::TRUE); } } } } transformed.or(or); } transformed } #[cfg(test)] mod tests { use super::*; use std::str::FromStr; use uv_pypi_types::ConflictSet; /// Creates a collection of declared conflicts from the sets /// provided. fn create_conflicts(it: impl IntoIterator) -> Conflicts { let mut conflicts = Conflicts::empty(); for set in it { conflicts.push(set); } conflicts } /// Creates a single set of conflicting items. /// /// For convenience, this always creates conflicting items with a package /// name of `foo` and with the given string as the extra name. fn create_set<'a>(it: impl IntoIterator) -> ConflictSet { let items = it .into_iter() .map(|extra| (create_package("pkg"), create_extra(extra))) .map(ConflictItem::from) .collect::>(); ConflictSet::try_from(items).unwrap() } /// Shortcut for creating a package name. fn create_package(name: &str) -> PackageName { PackageName::from_str(name).unwrap() } /// Shortcut for creating an extra name. fn create_extra(name: &str) -> ExtraName { ExtraName::from_str(name).unwrap() } /// Shortcut for creating a conflict marker from an extra name. fn create_extra_marker(name: &str) -> ConflictMarker { ConflictMarker::extra(&create_package("pkg"), &create_extra(name)) } /// Shortcut for creating a conflict item from an extra name. fn create_extra_item(name: &str) -> ConflictItem { ConflictItem::from((create_package("pkg"), create_extra(name))) } /// Shortcut for creating a conflict map. fn create_known_conflicts<'a>( it: impl IntoIterator, ) -> FxHashMap { it.into_iter() .map(|(extra, marker)| { ( create_extra_item(extra), MarkerTree::from_str(marker).unwrap(), ) }) .collect() } /// Returns a string representation of the given conflict marker. /// /// This is just the underlying marker. And if it's `true`, then a /// non-conforming `true` string is returned. (Which is fine since /// this is just for tests.) fn tostr(cm: ConflictMarker) -> String { cm.marker .try_to_string() .unwrap_or_else(|| "true".to_string()) } /// This tests the conversion from declared conflicts into a conflict /// marker. This is used to describe "world knowledge" about which /// extras/groups are and aren't allowed to be activated together. #[test] fn conflicts_as_marker() { let conflicts = create_conflicts([create_set(["foo", "bar"])]); let cm = ConflictMarker::from_conflicts(&conflicts); assert_eq!( tostr(cm), "extra != 'extra-3-pkg-foo' or extra != 'extra-3-pkg-bar'" ); let conflicts = create_conflicts([create_set(["foo", "bar", "baz"])]); let cm = ConflictMarker::from_conflicts(&conflicts); assert_eq!( tostr(cm), "(extra != 'extra-3-pkg-baz' and extra != 'extra-3-pkg-foo') \ or (extra != 'extra-3-pkg-bar' and extra != 'extra-3-pkg-foo') \ or (extra != 'extra-3-pkg-bar' and extra != 'extra-3-pkg-baz')", ); let conflicts = create_conflicts([create_set(["foo", "bar"]), create_set(["fox", "ant"])]); let cm = ConflictMarker::from_conflicts(&conflicts); assert_eq!( tostr(cm), "(extra != 'extra-3-pkg-bar' and extra != 'extra-3-pkg-fox') or \ (extra != 'extra-3-pkg-ant' and extra != 'extra-3-pkg-foo') or \ (extra != 'extra-3-pkg-ant' and extra != 'extra-3-pkg-bar') or \ (extra == 'extra-3-pkg-bar' and extra != 'extra-3-pkg-foo' and extra != 'extra-3-pkg-fox')", ); // I believe because markers are put into DNF, the marker we get here // is a lot bigger than what we might expect. Namely, this is how it's // constructed: // // (extra != 'extra-3-pkg-foo' or extra != 'extra-3-pkg-bar') // and (extra != 'extra-3-pkg-fox' or extra != 'extra-3-pkg-ant') // // In other words, you can't have both `foo` and `bar` active, and you // can't have both `fox` and `ant` active. But any other combination // is valid. So let's step through all of them to make sure the marker // below gives the expected result. (I did this because it's not at all // obvious to me that the above two markers are equivalent.) let disallowed = [ vec!["foo", "bar"], vec!["fox", "ant"], vec!["foo", "fox", "bar"], vec!["foo", "ant", "bar"], vec!["ant", "foo", "fox"], vec!["ant", "bar", "fox"], vec!["foo", "bar", "fox", "ant"], ]; for extra_names in disallowed { let extras = extra_names .iter() .copied() .map(|name| (create_package("pkg"), create_extra(name))) .collect::>(); let groups = Vec::<(PackageName, GroupName)>::new(); assert!( !cm.evaluate(&extras, &groups), "expected `{extra_names:?}` to evaluate to `false` in `{cm:?}`" ); } let allowed = [ vec![], vec!["foo"], vec!["bar"], vec!["fox"], vec!["ant"], vec!["foo", "fox"], vec!["foo", "ant"], vec!["bar", "fox"], vec!["bar", "ant"], ]; for extra_names in allowed { let extras = extra_names .iter() .copied() .map(|name| (create_package("pkg"), create_extra(name))) .collect::>(); let groups = Vec::<(PackageName, GroupName)>::new(); assert!( cm.evaluate(&extras, &groups), "expected `{extra_names:?}` to evaluate to `true` in `{cm:?}`" ); } } /// This tests conflict marker simplification after "imbibing" world /// knowledge about which extras/groups cannot be activated together. #[test] fn imbibe() { let conflicts = create_conflicts([create_set(["foo", "bar"])]); let conflicts_marker = ConflictMarker::from_conflicts(&conflicts); let foo = create_extra_marker("foo"); let bar = create_extra_marker("bar"); // In this case, we simulate a dependency whose conflict marker // is just repeating the fact that conflicting extras cannot // both be activated. So this one simplifies to `true`. let mut dep_conflict_marker = UniversalMarker::new(MarkerTree::TRUE, foo.negate().or(bar.negate())); assert_eq!( format!("{dep_conflict_marker:?}"), "extra != 'extra-3-pkg-foo' or extra != 'extra-3-pkg-bar'" ); dep_conflict_marker.imbibe(conflicts_marker); assert_eq!(format!("{dep_conflict_marker:?}"), "true"); } #[test] fn resolve() { let known_conflicts = create_known_conflicts([("foo", "sys_platform == 'darwin'")]); let cm = MarkerTree::from_str("(python_version >= '3.10' and extra == 'extra-3-pkg-foo') or (python_version < '3.10' and extra != 'extra-3-pkg-foo')").unwrap(); let cm = resolve_conflicts(cm, &known_conflicts); assert_eq!( cm.try_to_string().as_deref(), Some( "(python_full_version < '3.10' and sys_platform != 'darwin') or (python_full_version >= '3.10' and sys_platform == 'darwin')" ) ); let cm = MarkerTree::from_str("python_version >= '3.10' and extra == 'extra-3-pkg-foo'") .unwrap(); let cm = resolve_conflicts(cm, &known_conflicts); assert_eq!( cm.try_to_string().as_deref(), Some("python_full_version >= '3.10' and sys_platform == 'darwin'") ); let cm = MarkerTree::from_str("python_version >= '3.10' and extra == 'extra-3-pkg-bar'") .unwrap(); let cm = resolve_conflicts(cm, &known_conflicts); assert!(cm.is_false()); } }