Fix VersionSpec globs (#3889)

This commit is contained in:
Antoine Prouvost 2025-04-18 08:32:17 +02:00 committed by GitHub
parent 10036e83e7
commit 5f4759e271
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 692 additions and 66 deletions

View File

@ -40,9 +40,10 @@ Breaking changes include:
- A new config ``order_solver_request`` (default true) can be used to order the dependencies passed
to the solver, getting order independent solutions.
- Support for complex match specs such as ``pkg[md5=0000000000000]`` and ``pkg[build='^\d*$']``.
- **Temporary regression:** Lost support for leading and internal globs in
- **For ``micromamba<=2.1.0``:** Lost support for leading and internal globs in
version strings (via redesigned ``VersionSpec``, which no longer handles
version strings as a regex). Only trailing globs are supported at the moment.
version strings as a regex). Only trailing globs were supported.
- Full version regexes, such as ``^3.3+$``, are not implemented.
.. TODO OCI and mirrors
@ -89,9 +90,11 @@ Changes include:
- The redesign of ``MatchSpec``.
The module also includes a platform enumeration, an implementation of ordered ``Version``,
and a ``VersionSpec`` to match versions.
**Breaking change (temporary regression):** ``VersionSpec`` lost support for
**Breaking change (for ``libmambapy<=2.1.0``):** ``VersionSpec`` lost support for
leading and internal globs in version strings because they are no longer
handled as a regex. Only trailing globs are supported at the moment.
handled as a regex. Only trailing globs were supported.
- Full version regexes, such as ``^3.3+$``, are not implemented in ``VersionSpec`` and
``MatchSpec``.
- ``PackageInfo`` has been moved to this submodule.
Some attributes have been given a more explicit name ``fn`` > ``filename``,
``url`` > ``package_url``.
@ -133,11 +136,13 @@ The main changes are:
- A refactoring of a purely functional ``Channel`` class,
- Implementation of a ``UnresolvedChannel`` to describe unresolved ``Channels``,
- A refactored and complete implementation of ``MatchSpec`` using the components above.
- **Breaking change (temporary regression):** ``VersionSpec`` lost support for
- **Breaking change (for ``libmamba<=2.1.0``):** ``VersionSpec`` lost support for
leading and internal globs in version strings because they are no longer
handled as a regex. Only trailing globs are supported at the moment. This
handled as a regex. Only trailing globs were supported. This
affects version strings in both the command-line interface and recipe
requirements.
- Full version regexes, such as ``^3.3+$``, are not implemented in ``VersionSpec`` and
``MatchSpec``.
- A cleanup of ``ChannelContext`` to be a light proxy and parameter holder wrapping the
``specs::Channel``.

View File

@ -131,6 +131,14 @@ namespace mamba::specs
*/
[[nodiscard]] auto str(std::size_t level) const -> std::string;
/**
* String representation that treats ``*`` as glob pattern.
*
* Instead of printing them as ``0*`` (as a special literal), it formats them as ``*``.
* In full, a version like ``*.1.*`` will print as such instead of ``0*.1.0*``.
*/
[[nodiscard]] auto str_glob() const -> std::string;
[[nodiscard]] auto operator==(const Version& other) const -> bool;
[[nodiscard]] auto operator!=(const Version& other) const -> bool;
[[nodiscard]] auto operator<(const Version& other) const -> bool;
@ -184,7 +192,19 @@ struct fmt::formatter<mamba::specs::VersionPartAtom>
template <>
struct fmt::formatter<mamba::specs::Version>
{
enum struct FormatType
{
Normal,
/**
* The Glob pattern, as used internally ``VersionPredicate``, lets you treat ``*`` as a
* glob pattern instead of the special character.
* It lets you format ``*.*`` as such instead of ``0*.0*``.
*/
Glob,
};
std::optional<std::size_t> m_level;
FormatType m_type = FormatType::Normal;
auto parse(format_parse_context& ctx) -> decltype(ctx.begin());

View File

@ -39,6 +39,8 @@ namespace mamba::specs
[[nodiscard]] static auto make_not_starts_with(Version ver) -> VersionPredicate;
[[nodiscard]] static auto make_compatible_with(Version ver, std::size_t level)
-> VersionPredicate;
[[nodiscard]] static auto make_version_glob(Version pattern) -> VersionPredicate;
[[nodiscard]] static auto make_not_version_glob(Version pattern) -> VersionPredicate;
/** Construct an free interval. */
VersionPredicate() = default;
@ -48,6 +50,13 @@ namespace mamba::specs
*/
[[nodiscard]] auto contains(const Version& point) const -> bool;
/**
* True if it contains a glob or negative glob expression.
*
* Does not return true for predicates that could be written as globs but are not.
*/
[[nodiscard]] auto has_glob() const -> bool;
[[nodiscard]] auto str() const -> std::string;
/**
@ -80,6 +89,16 @@ namespace mamba::specs
auto operator()(const Version&, const Version&) const -> bool;
};
struct version_glob
{
auto operator()(const Version&, const Version&) const -> bool;
};
struct not_version_glob
{
auto operator()(const Version&, const Version&) const -> bool;
};
/**
* Operator to compare with the stored version.
*
@ -87,6 +106,10 @@ namespace mamba::specs
* ``VersionSpec`` parsing (hence not user-extensible), and performance-sensitive,
* we choose an ``std::variant`` for dynamic dispatch.
* An alternative could be a type-erased wrapper with local storage.
*
* Not alternatives (``not_starts_with``, ``not_version_glob``) could also be implemented
* as a not operator in VersionSpec rather than a predicate, but they are used often enough
* to deserve their specialization.
*/
using BinaryOperator = std::variant<
free_interval,
@ -98,8 +121,15 @@ namespace mamba::specs
std::less_equal<Version>,
starts_with,
not_starts_with,
compatible_with>;
compatible_with,
not_version_glob,
version_glob>;
// Originally, with only stateless operators, it made sense to have the version factored
// in this class' attributes. However, with additions of variants that use the version
// for a different meaning (version_glob, compatible_with), or not at all (free interval),
// it would make sense to move it to each individual class for better scoping (e.g. see
// this class' operator==).
Version m_version = {};
BinaryOperator m_operator = free_interval{};
@ -109,6 +139,8 @@ namespace mamba::specs
friend auto operator==(starts_with, starts_with) -> bool;
friend auto operator==(not_starts_with, not_starts_with) -> bool;
friend auto operator==(compatible_with, compatible_with) -> bool;
friend auto operator==(version_glob, version_glob) -> bool;
friend auto operator==(not_version_glob, not_version_glob) -> bool;
friend auto operator==(const VersionPredicate& lhs, const VersionPredicate& rhs) -> bool;
friend struct ::fmt::formatter<VersionPredicate>;
};
@ -149,7 +181,7 @@ namespace mamba::specs
static constexpr std::string_view less_equal_str = "<=";
static constexpr std::string_view compatible_str = "~=";
static constexpr std::string_view glob_suffix_str = ".*";
static constexpr char glob_suffix_token = '*';
static constexpr std::string_view glob_pattern_str = "*";
[[nodiscard]] static auto parse(std::string_view str) -> expected_parse_t<VersionSpec>;
@ -171,6 +203,12 @@ namespace mamba::specs
*/
[[nodiscard]] auto is_explicitly_free() const -> bool;
/**
* True if it contains a glob or negative glob expression anywhere in the tree.
*
* Does not return true for predicates that could be written as globs but are not.
*/
[[nodiscard]] auto has_glob() const -> bool;
/**
* A string representation of the version spec.
*

View File

@ -1023,6 +1023,7 @@ namespace mamba::specs
// Based on what libsolv and conda_build_form can handle.
// Glob in names and build_string are fine
return (version().expression_size() <= 3) // includes op so e.g. ``>3,<4``
&& !version().has_glob() //
&& build_number().is_explicitly_free() //
&& build_string().is_glob() //
&& !channel().has_value() //

View File

@ -15,6 +15,8 @@
#include "mamba/util/cast.hpp"
#include "mamba/util/string.hpp"
#include "specs/version_spec_impl.hpp"
namespace mamba::specs
{
namespace
@ -239,6 +241,11 @@ namespace mamba::specs
return fmt::format(fmt, *this);
}
auto Version::str_glob() const -> std::string
{
return fmt::format("{:g}", *this);
}
namespace
{
/**
@ -775,18 +782,36 @@ namespace mamba::specs
auto
fmt::formatter<mamba::specs::Version>::parse(format_parse_context& ctx) -> decltype(ctx.begin())
{
// make sure that range is not empty
if (ctx.begin() == ctx.end() || *ctx.begin() == '}')
const auto end = ctx.end();
const auto start = ctx.begin();
// Make sure that range is not empty
if (start == end || *start == '}')
{
return ctx.begin();
return start;
}
// Check for restricted number of segments at beginning
std::size_t val = 0;
auto [ptr, ec] = std::from_chars(ctx.begin(), ctx.end(), val);
if (ec != std::errc())
auto [ptr, ec] = std::from_chars(start, end, val);
if (ec == std::errc())
{
throw fmt::format_error("Invalid format" + std::string(ctx.begin(), ctx.end()));
m_level = val;
}
m_level = val;
// Check for end of format spec
if (ptr == end || *ptr == '}')
{
return ptr;
}
// Check the custom format type
if (*ptr == 'g')
{
m_type = FormatType::Glob;
++ptr;
}
return ptr;
}
@ -812,9 +837,16 @@ fmt::formatter<mamba::specs::Version>::format(const ::mamba::specs::Version v, f
}
if (i < version.size())
{
for (const auto& atom : version[i])
if (m_type == FormatType::Glob && version[i] == mamba::specs::VERSION_GLOB_SEGMENT)
{
l_out = fmt::format_to(l_out, "{}", atom);
l_out = fmt::format_to(l_out, "{}", mamba::specs::GLOB_PATTERN_STR);
}
else
{
for (const auto& atom : version[i])
{
l_out = fmt::format_to(l_out, "{}", atom);
}
}
}
else

View File

@ -7,12 +7,16 @@
#include <algorithm>
#include <array>
#include <type_traits>
#include <variant>
#include <fmt/format.h>
#include <fmt/ranges.h>
#include "mamba/specs/version_spec.hpp"
#include "mamba/util/string.hpp"
#include "specs/version_spec_impl.hpp"
namespace mamba::specs
{
/*****************************************************
@ -65,6 +69,104 @@ namespace mamba::specs
return lhs.level == rhs.level;
}
namespace
{
auto version_match_glob(const CommonVersion& candidate, const CommonVersion& pattern) -> bool
{
auto cand_it = candidate.cbegin();
const auto cand_last = candidate.cend();
auto pat_it = pattern.cbegin();
const auto pat_last = pattern.cend();
auto parts_required = std::size_t(0);
auto parts_available = std::size_t(0);
constexpr auto is_failed = [](auto req, auto avail) -> bool
{ return (req > avail) || (req == 0 && avail > 0); };
constexpr auto is_glob_part = [](const auto& p) -> bool
{ return p == VERSION_GLOB_SEGMENT; };
constexpr auto distance = [](auto i1, auto i2)
{
assert(i1 <= i2);
return static_cast<std::size_t>(std::distance(i1, i2));
};
while (pat_it != pat_last)
{
// We move forward in the pattern counting all the contiguous glob parts.
const auto pat_sub_first = std::find_if_not(pat_it, pat_last, is_glob_part);
parts_required += distance(pat_it, pat_sub_first);
pat_it = pat_sub_first;
// No more explicit subpatterns (i.e. not globs) to match
if (pat_it == pat_last)
{
break;
}
// Find the end of the sub pattern, i.e. to the start of the next glob.
// This is required to avoid greedily matching on the first similar character.
const auto pat_sub_last = std::find_if(pat_sub_first, pat_last, is_glob_part);
// At this point we have a required pattern.
// We search for it in the given version and count the parts that were skipped.
const auto cand_sub_first = std::search(cand_it, cand_last, pat_sub_first, pat_sub_last);
parts_available += distance(cand_it, cand_sub_first);
cand_it = cand_sub_first;
// If we exhause the candidate without finding a match for the pattern it's a
// failure
if (cand_it == cand_last)
{
return false;
}
// At this point we have a match.
// We compare the number of globs found with the number of unmatched candidate parts
if (is_failed(parts_required, parts_available))
{
return false;
}
// We pass through the match and reset the counts
const auto subpat_len = std::distance(pat_sub_first, pat_sub_last);
pat_it += subpat_len;
cand_it += subpat_len;
parts_required = 0;
parts_available = 0;
}
parts_available += static_cast<std::size_t>(std::distance(cand_it, cand_last));
return !is_failed(parts_required, parts_available);
}
}
auto VersionPredicate::version_glob::operator()(const Version& point, const Version& pattern) const
-> bool
{
return (point.epoch() == pattern.epoch())
&& version_match_glob(point.version(), pattern.version())
&& version_match_glob(point.local(), pattern.local());
}
auto operator==(VersionPredicate::version_glob, VersionPredicate::version_glob) -> bool
{
return true;
}
auto
VersionPredicate::not_version_glob::operator()(const Version& point, const Version& pattern) const
-> bool
{
return !VersionPredicate::version_glob{}(point, pattern);
}
auto operator==(VersionPredicate::not_version_glob, VersionPredicate::not_version_glob) -> bool
{
return true;
}
static auto operator==(std::equal_to<Version>, std::equal_to<Version>) -> bool
{
return true;
@ -104,6 +206,12 @@ namespace mamba::specs
return std::visit([&](const auto& op) { return op(point, m_version); }, m_operator);
}
auto VersionPredicate::has_glob() const -> bool
{
return std::holds_alternative<VersionPredicate::version_glob>(m_operator)
|| std::holds_alternative<VersionPredicate::not_version_glob>(m_operator);
}
auto VersionPredicate::make_free() -> VersionPredicate
{
return VersionPredicate({}, free_interval{});
@ -154,6 +262,16 @@ namespace mamba::specs
return VersionPredicate(std::move(ver), compatible_with{ level });
}
auto VersionPredicate::make_version_glob(Version pattern) -> VersionPredicate
{
return VersionPredicate(std::move(pattern), version_glob{});
}
auto VersionPredicate::make_not_version_glob(Version pattern) -> VersionPredicate
{
return VersionPredicate(std::move(pattern), not_version_glob{});
}
auto VersionPredicate::str() const -> std::string
{
return fmt::format("{}", *this);
@ -172,7 +290,15 @@ namespace mamba::specs
auto operator==(const VersionPredicate& lhs, const VersionPredicate& rhs) -> bool
{
return (lhs.m_operator == rhs.m_operator) && (lhs.m_version == rhs.m_version);
return (lhs.m_operator == rhs.m_operator) //
&& (lhs.m_version == rhs.m_version)
// In version_glob, the version is not understood purely as a version since ``*``
// has different meaning, as explicit trailing zeros.
// Versions should be made part of this variant internal and handled there.
// On the different meanings of ``*``, see this CEP:
// https://github.com/conda/ceps/pull/60
&& (!std::holds_alternative<VersionPredicate::version_glob>(lhs.m_operator)
|| lhs.m_version.version().size() == rhs.m_version.version().size());
}
auto operator!=(const VersionPredicate& lhs, const VersionPredicate& rhs) -> bool
@ -210,12 +336,7 @@ fmt::formatter<mamba::specs::VersionPredicate>::format(
using Op = std::decay_t<decltype(op)>;
if constexpr (std::is_same_v<Op, VersionPredicate::free_interval>)
{
out = fmt::format_to(
out,
"{}{}",
VersionSpec::starts_with_str,
VersionSpec::glob_suffix_token
);
out = fmt::format_to(out, "{}", VersionSpec::preferred_free_str);
}
if constexpr (std::is_same_v<Op, std::equal_to<Version>>)
{
@ -274,6 +395,14 @@ fmt::formatter<mamba::specs::VersionPredicate>::format(
pred.m_version.str(format_level)
);
}
if constexpr (std::is_same_v<Op, VersionPredicate::version_glob>)
{
out = fmt::format_to(out, "{:g}", pred.m_version);
}
if constexpr (std::is_same_v<Op, VersionPredicate::not_version_glob>)
{
out = fmt::format_to(out, "{}{:g}", VersionSpec::not_equal_str, pred.m_version);
}
},
pred.m_operator
);
@ -311,6 +440,28 @@ namespace mamba::specs
return m_tree.empty() || ((m_tree.size() == 1) && m_tree.evaluate(is_free_pred));
}
auto VersionSpec::has_glob() const -> bool
{
if (expression_size() == 0)
{
return false;
}
auto found = false;
m_tree.infix_for_each(
[&found](const auto& elem)
{
using Elem = std::decay_t<decltype(elem)>;
if constexpr (std::is_same_v<Elem, VersionPredicate>)
{
found |= elem.has_glob();
}
}
);
return found;
}
auto VersionSpec::str() const -> std::string
{
return fmt::format("{}", *this);
@ -337,6 +488,8 @@ namespace mamba::specs
auto parse_op_and_version(std::string_view str) -> expected_parse_t<VersionPredicate>
{
str = util::strip(str);
// Conda-forge repodata.json bug with trailing `.` in `openblas 0.2.18|0.2.18.*.`
str = util::remove_suffix(str, Version::part_delim);
// WARNING order is important since some operator are prefix of others.
if (str.empty() || equal_any(str, VersionSpec::all_free_strs))
{
@ -379,8 +532,24 @@ namespace mamba::specs
}
);
}
// A simple `.*` check on the end of the version spec string
const bool has_glob_suffix = util::ends_with(str, VersionSpec::glob_suffix_str);
const std::size_t glob_len = has_glob_suffix * VersionSpec::glob_suffix_str.size();
const std::size_t glob_suffix_active_len = has_glob_suffix
* VersionSpec::glob_suffix_str.size();
// A more complex glob type of glob used that requires a glob predicate.
// Needs to be used after stripping a potential leading operator as it may lead
// to positive or negative glob.
// The check for whether the "*" is a glob or a valid version character is poorly
// defined.
constexpr auto has_complex_glob = [](std::string_view expr) -> bool
{
constexpr auto glob_suffix_len = VersionSpec::glob_suffix_str.size();
return util::starts_with(expr, VersionSpec::glob_pattern_str)
|| (expr.find(VersionSpec::glob_suffix_str)
< std::max(expr.size(), glob_suffix_len) - glob_suffix_len);
};
if (util::starts_with(str, VersionSpec::equal_str))
{
const std::size_t start = VersionSpec::equal_str.size();
@ -388,7 +557,9 @@ namespace mamba::specs
if (has_glob_suffix)
{
return Version::parse(
util::lstrip(str.substr(start, str.size() - glob_len - start))
util::lstrip(
str.substr(start, str.size() - glob_suffix_active_len - start)
)
)
.transform([](specs::Version&& ver)
{ return VersionPredicate::make_starts_with(std::move(ver)); });
@ -402,12 +573,22 @@ namespace mamba::specs
}
if (util::starts_with(str, VersionSpec::not_equal_str))
{
const std::size_t start = VersionSpec::not_equal_str.size();
constexpr std::size_t start = VersionSpec::not_equal_str.size();
const auto str_no_op = util::lstrip(str.substr(start));
// Glob changes meaning for !=1.*.0
if (has_complex_glob(str_no_op))
{
return Version::parse(str_no_op).transform(
[](specs::Version&& ver)
{ return VersionPredicate::make_not_version_glob(std::move(ver)); }
);
}
// Glob suffix changes meaning for !=1.3.*
if (has_glob_suffix)
else if (has_glob_suffix)
{
return Version::parse(
util::lstrip(str.substr(start, str.size() - glob_len - start))
str_no_op.substr(0, str_no_op.size() - glob_suffix_active_len)
)
.transform([](specs::Version&& ver)
{ return VersionPredicate::make_not_starts_with(std::move(ver)); }
@ -415,19 +596,38 @@ namespace mamba::specs
}
else
{
return Version::parse(util::lstrip(str.substr(start)))
.transform([](specs::Version&& ver)
{ return VersionPredicate::make_not_equal_to(std::move(ver)); });
return Version::parse(str_no_op).transform(
[](specs::Version&& ver)
{ return VersionPredicate::make_not_equal_to(std::move(ver)); }
);
}
}
if (util::starts_with(str, VersionSpec::starts_with_str))
{
const std::size_t start = VersionSpec::starts_with_str.size();
constexpr std::size_t start = VersionSpec::starts_with_str.size();
const auto str_no_op = util::lstrip(str.substr(start));
// Glob changes meaning for =1.*.0
if (has_complex_glob(str_no_op))
{
return Version::parse(str_no_op).transform(
[](specs::Version&& ver)
{ return VersionPredicate::make_version_glob(std::move(ver)); }
);
}
// Glob suffix does not change meaning for =1.3.*
return Version::parse(util::lstrip(str.substr(start, str.size() - glob_len - start)))
return Version::parse(str_no_op.substr(0, str_no_op.size() - glob_suffix_active_len))
.transform([](specs::Version&& ver)
{ return VersionPredicate::make_starts_with(std::move(ver)); });
}
// If we find a glob suffix as in `3.*`, we leave it to be processed as a
// ``starts_with`` in the next block.
if (has_complex_glob(str))
{
return Version::parse(util::lstrip(str))
.transform([](specs::Version&& ver)
{ return VersionPredicate::make_version_glob(std::move(ver)); });
}
// All versions must start with either a digit or a lowercase letter
// The version regex should comply with r"^[\*\.\+!_0-9a-z]+$"
// cf. https://github.com/conda/conda/blob/main/conda/models/version.py#L33
@ -437,11 +637,11 @@ namespace mamba::specs
if (util::is_digit(str.front()) || util::is_lower(str.front()))
{
// Glob suffix does change meaning for 1.3.* and 1.3*
if (util::ends_with(str, VersionSpec::glob_suffix_token))
if (util::ends_with(str, VersionSpec::glob_suffix_str.back()))
{
// either ".*" or "*"
static constexpr auto one = std::size_t(1); // MSVC
const std::size_t len = str.size() - std::max(glob_len, one);
const std::size_t len = str.size() - std::max(glob_suffix_active_len, one);
return Version::parse(util::lstrip(str.substr(0, len)))
.transform([](specs::Version&& ver)
{ return VersionPredicate::make_starts_with(std::move(ver)); });

View File

@ -0,0 +1,24 @@
#ifndef MAMBA_SPECS_VERSION_SPEC_IMPL_HPP
#define MAMBA_SPECS_VERSION_SPEC_IMPL_HPP
#include "mamba/specs/version.hpp"
#include "mamba/specs/version_spec.hpp"
namespace mamba::specs
{
/**
* Formatting of ``VersionSpec`` version glob relies on special handling in ``Version``
* formatter.
* Said behaviour should not be surprising, this file however serves to make it clear why and
* how this functionality was added to ``Version``.
*/
inline static const auto VERSION_GLOB_SEGMENT = VersionPart(
{ { 0, VersionSpec::glob_pattern_str } }
);
inline static constexpr std::string_view GLOB_PATTERN_STR = VersionSpec::glob_pattern_str;
}
#endif

View File

@ -4,11 +4,15 @@
//
// The full license is in the file LICENSE, distributed with this software.
#include <fstream>
#include <catch2/catch_all.hpp>
#include <nlohmann/json.hpp>
#include "mamba/specs/match_spec.hpp"
#include "mamba/specs/package_info.hpp"
#include "mamba/util/build.hpp"
#include "mamba/util/environment.hpp"
#include "mamba/util/string.hpp"
using namespace mamba;
@ -18,7 +22,7 @@ namespace
{
using PlatformSet = typename util::flat_set<std::string>;
TEST_CASE("MatchSpec parse")
TEST_CASE("MatchSpec parse", "[mamba::specs][mamba::specs::MatchSpec]")
{
SECTION("<empty>")
{
@ -142,10 +146,10 @@ namespace
REQUIRE(ms2 == ms);
}
// Invalid case from `inform2w64-sysroot_win-64-v12.0.0.r2.ggc561118da-h707e725_0.conda`
// which is currently supported but which must not.
SECTION("mingw-w64-ucrt-x86_64-crt-git v12.0.0.r2.ggc561118da h707e725_0")
{
// Invalid case from `inform2w64-sysroot_win-64-v12.0.0.r2.ggc561118da-h707e725_0.conda`
// which is currently supported but which must not.
auto ms = MatchSpec::parse("mingw-w64-ucrt-x86_64-crt-git v12.0.0.r2.ggc561118da h707e725_0")
.value();
REQUIRE(ms.name().str() == "mingw-w64-ucrt-x86_64-crt-git");
@ -155,6 +159,15 @@ namespace
REQUIRE(ms.str() == "mingw-w64-ucrt-x86_64-crt-git==0v12.0.0.0r2.0ggc561118da=h707e725_0");
}
SECTION("openblas 0.2.18|0.2.18.*.")
{
// Invalid case from `inform2w64-sysroot_win-64-v12.0.0.r2.ggc561118da-h707e725_0.conda`
// which is currently supported but which must not.
auto ms = MatchSpec::parse("openblas 0.2.18|0.2.18.*.").value();
REQUIRE(ms.name().str() == "openblas");
REQUIRE(ms.version().str() == "==0.2.18|=0.2.18");
}
SECTION("_libgcc_mutex 0.1 conda_forge")
{
auto ms = MatchSpec::parse("_libgcc_mutex 0.1 conda_forge").value();
@ -667,7 +680,7 @@ namespace
}
}
TEST_CASE("parse_url")
TEST_CASE("parse_url", "[mamba::specs][mamba::specs::MatchSpec]")
{
SECTION("https://conda.com/pkg-2-bld.conda")
{
@ -714,7 +727,7 @@ namespace
}
}
TEST_CASE("Conda discrepancies")
TEST_CASE("Conda discrepancies", "[mamba::specs][mamba::specs::MatchSpec]")
{
SECTION("python=3.7=bld")
{
@ -742,7 +755,7 @@ namespace
}
}
TEST_CASE("is_simple")
TEST_CASE("is_simple", "[mamba::specs][mamba::specs::MatchSpec]")
{
SECTION("Positive")
{
@ -780,7 +793,7 @@ namespace
}
}
TEST_CASE("MatchSpec::contains")
TEST_CASE("MatchSpec::contains", "[mamba::specs][mamba::specs::MatchSpec]")
{
// Note that tests for individual ``contains`` functions (``VersionSpec::contains``,
// ``BuildNumber::contains``, ``GlobSpec::contains``...) are tested in their respective
@ -864,6 +877,14 @@ namespace
REQUIRE_FALSE(ms.contains_except_channel(pkg));
}
SECTION("name *,*.* build*")
{
const auto ms = "name *,*.* build*"_ms;
REQUIRE(ms.contains_except_channel(Pkg{ "name", "3.7"_v, "build_foo" }));
REQUIRE_FALSE(ms.contains_except_channel(Pkg{ "name", "3"_v, "build_foo" }));
REQUIRE_FALSE(ms.contains_except_channel(Pkg{ "name", "3.7"_v, "bar" }));
}
SECTION("pkg[md5=helloiamnotreallymd5haha]")
{
const auto ms = "pkg[md5=helloiamnotreallymd5haha]"_ms;
@ -1205,9 +1226,96 @@ namespace
/* .track_features =*/{},
}));
}
SECTION("python=3.*")
{
const auto ms = "python=3.*"_ms;
REQUIRE(ms.contains_except_channel(Pkg{
/* .name= */ "python",
/* .version= */ "3.12.0"_v,
/* .build_string= */ "bld",
/* .build_number= */ 0,
/* .md5= */ "lemd5",
/* .sha256= */ "somesha256",
/* .license= */ "some-license",
/* .platform= */ "linux-64",
/* .track_features =*/{},
}));
REQUIRE_FALSE(ms.contains_except_channel(Pkg{
/* .name= */ "python",
/* .version= */ "2.7.12"_v,
/* .build_string= */ "bld",
/* .build_number= */ 0,
/* .md5= */ "lemd5",
/* .sha256= */ "somesha256",
/* .license= */ "some-license",
/* .platform= */ "linux-64",
/* .track_features =*/{},
}));
}
SECTION("python=3.*.1")
{
const auto ms = "python=3.*.1"_ms;
REQUIRE(ms.contains_except_channel(Pkg{
/* .name= */ "python",
/* .version= */ "3.12.1"_v,
/* .build_string= */ "bld",
/* .build_number= */ 0,
/* .md5= */ "lemd5",
/* .sha256= */ "somesha256",
/* .license= */ "some-license",
/* .platform= */ "linux-64",
/* .track_features =*/{},
}));
REQUIRE_FALSE(ms.contains_except_channel(Pkg{
/* .name= */ "python",
/* .version= */ "3.12.0"_v,
/* .build_string= */ "bld",
/* .build_number= */ 0,
/* .md5= */ "lemd5",
/* .sha256= */ "somesha256",
/* .license= */ "some-license",
/* .platform= */ "linux-64",
/* .track_features =*/{},
}));
}
SECTION("python=*.13.1")
{
const auto ms = "python=*.13.1"_ms;
REQUIRE(ms.contains_except_channel(Pkg{
/* .name= */ "python",
/* .version= */ "3.13.1"_v,
/* .build_string= */ "bld",
/* .build_number= */ 0,
/* .md5= */ "lemd5",
/* .sha256= */ "somesha256",
/* .license= */ "some-license",
/* .platform= */ "linux-64",
/* .track_features =*/{},
}));
REQUIRE_FALSE(ms.contains_except_channel(Pkg{
/* .name= */ "python",
/* .version= */ "3.12.0"_v,
/* .build_string= */ "bld",
/* .build_number= */ 0,
/* .md5= */ "lemd5",
/* .sha256= */ "somesha256",
/* .license= */ "some-license",
/* .platform= */ "linux-64",
/* .track_features =*/{},
}));
}
}
TEST_CASE("MatchSpec comparability and hashability")
TEST_CASE("MatchSpec comparability and hashability", "[mamba::specs][mamba::specs::MatchSpec]")
{
using namespace specs::match_spec_literals;
using namespace specs::version_literals;
@ -1233,4 +1341,61 @@ namespace
REQUIRE(spec1_hash == spec2_hash);
REQUIRE(spec1_hash != spec3_hash);
}
auto repodata_all_depends(const std::string& path)
-> std::vector<std::tuple<std::string, std::string>>
{
auto input = std::ifstream(path);
if (!input.is_open())
{
throw std::runtime_error("Failed to open file: " + std::string(path));
}
auto j = nlohmann::json::parse(input);
if (!j.contains("packages") || !j["packages"].is_object())
{
throw std::runtime_error(R"(Missing or invalid "packages" field)");
}
auto result = std::vector<std::tuple<std::string, std::string>>();
for (const auto& [pkg_name, pkg] : j["packages"].items())
{
if (!pkg.contains("depends") || !pkg["depends"].is_array())
{
throw std::runtime_error(R"(Missing or invalid "depends" in package)");
}
for (const auto& dep : pkg["depends"])
{
if (!dep.is_string())
{
throw std::runtime_error(R"(Non-string entry in "depends")");
}
result.emplace_back(pkg_name, dep.get<std::string>());
}
}
return result;
}
TEST_CASE("Repodata MatchSpec::parse", "[mamba::specs][mamba::specs::MatchSpec][.integration]")
{
const auto all_ms_str = []()
{
if (const auto path = util::get_env("MAMBA_TEST_REPODATA_JSON"))
{
return repodata_all_depends(path.value());
}
throw std::runtime_error(R"(Please define "MAMBA_TEST_REPODATA_JSON")");
}();
for (const auto& [pkg_name, ms_str] : all_ms_str)
{
CAPTURE(pkg_name);
CAPTURE(ms_str);
REQUIRE(MatchSpec::parse(ms_str).has_value());
}
}
}

View File

@ -17,7 +17,7 @@ using namespace mamba::specs;
namespace
{
TEST_CASE("atom_comparison")
TEST_CASE("atom_comparison", "[mamba::specs][mamba::specs::Version]")
{
// No literal
REQUIRE(VersionPartAtom(1) == VersionPartAtom(1, ""));
@ -59,13 +59,13 @@ namespace
REQUIRE(std::adjacent_find(sorted_atoms.cbegin(), sorted_atoms.cend()) == sorted_atoms.cend());
}
TEST_CASE("atom_format")
TEST_CASE("atom_format", "[mamba::specs][mamba::specs::Version]")
{
REQUIRE(VersionPartAtom(1, "dev").str() == "1dev");
REQUIRE(VersionPartAtom(2).str() == "2");
}
TEST_CASE("version_comparison")
TEST_CASE("version_comparison", "[mamba::specs][mamba::specs::Version]")
{
auto v = Version(0, { { { 1, "post" } } });
REQUIRE(v.version().size() == 1);
@ -97,7 +97,7 @@ namespace
REQUIRE(Version(0, { { { 11 }, { 0 }, { 0, "post" } } }) >= Version(0, { { { 2 }, { 0 } } }));
}
TEST_CASE("Version starts_with")
TEST_CASE("Version starts_with", "[mamba::specs][mamba::specs::Version]")
{
SECTION("positive")
{
@ -188,7 +188,7 @@ namespace
}
}
TEST_CASE("compatible_with")
TEST_CASE("compatible_with", "[mamba::specs][mamba::specs::Version]")
{
SECTION("positive")
{
@ -303,11 +303,11 @@ namespace
}
}
TEST_CASE("version_format")
TEST_CASE("version_format", "[mamba::specs][mamba::specs::Version]")
{
SECTION("11a0post.3.4dev")
{
auto v = Version(0, { { { 11, "a" }, { 0, "post" } }, { { 3 } }, { { 4, "dev" } } });
const auto v = Version(0, { { { 11, "a" }, { 0, "post" } }, { { 3 } }, { { 4, "dev" } } });
REQUIRE(v.str() == "11a0post.3.4dev");
REQUIRE(v.str(1) == "11a0post");
REQUIRE(v.str(2) == "11a0post.3");
@ -318,7 +318,7 @@ namespace
SECTION("1!11a0.3.4dev")
{
auto v = Version(1, { { { 11, "a" }, { 0 } }, { { 3 } }, { { 4, "dev" } } });
const auto v = Version(1, { { { 11, "a" }, { 0 } }, { { 3 } }, { { 4, "dev" } } });
REQUIRE(v.str() == "1!11a0.3.4dev");
REQUIRE(v.str(1) == "1!11a0");
REQUIRE(v.str(2) == "1!11a0.3");
@ -328,7 +328,7 @@ namespace
SECTION("1!11a0.3.4dev+1.2")
{
auto v = Version(
const auto v = Version(
1,
{ { { 11, "a" }, { 0 } }, { { 3 } }, { { 4, "dev" } } },
{ { { 1 } }, { { 2 } } }
@ -339,6 +339,17 @@ namespace
REQUIRE(v.str(3) == "1!11a0.3.4dev+1.2.0");
REQUIRE(v.str(4) == "1!11a0.3.4dev.0+1.2.0.0");
}
SECTION("*.1.*")
{
const auto v = Version(0, { { { 0, "*" } }, { { 1 } }, { { 0, "*" } } }, {});
REQUIRE(v.str() == "0*.1.0*");
REQUIRE(v.str(1) == "0*");
REQUIRE(v.str(2) == "0*.1");
REQUIRE(v.str(3) == "0*.1.0*");
REQUIRE(v.str(4) == "0*.1.0*.0");
REQUIRE(v.str_glob() == "*.1.*");
}
}
/**
@ -346,7 +357,7 @@ namespace
*
* @see https://github.com/conda/conda/blob/main/tests/models/test_version.py
*/
TEST_CASE("Version parse")
TEST_CASE("Version parse", "[mamba::specs][mamba::specs::Version]")
{
// clang-format off
auto sorted_version = std::vector<std::pair<std::string_view, Version>>{
@ -430,9 +441,22 @@ namespace
REQUIRE(Version::parse("0.4").value() != Version::parse("0.4.1"));
REQUIRE(Version::parse("0.4.a1").value() == Version::parse("0.4.0a1"));
REQUIRE(Version::parse("0.4.a1").value() != Version::parse("0.4.1a1"));
// These are valid versions with the special '*' ordering AND they are also used as such
// with version globs in VersionSpec
REQUIRE(Version::parse("*") == Version(0, { { { 0, "*" } } }));
REQUIRE(Version::parse("*.*") == Version(0, { { { 0, "*" } }, { { 0, "*" } } }));
REQUIRE(
Version::parse("*.*.*") == Version(0, { { { 0, "*" } }, { { 0, "*" } }, { { 0, "*" } } })
);
REQUIRE(
Version::parse("*.*.2023.12")
== Version(0, { { { 0, "*" } }, { { 0, "*" } }, { { 2023, "" } }, { { 12, "" } } })
);
REQUIRE(Version::parse("1.*") == Version(0, { { { 1, "" } }, { { 0, "*" } } }));
}
TEST_CASE("parse_invalid")
TEST_CASE("parse_invalid", "[mamba::specs][mamba::specs::Version]")
{
// Wrong epoch
REQUIRE_FALSE(Version::parse("!1.1").has_value());
@ -481,7 +505,7 @@ namespace
*
* @see https://github.com/conda/conda/blob/main/tests/models/test_version.py
*/
TEST_CASE("parse_openssl")
TEST_CASE("parse_openssl", "[mamba::specs][mamba::specs::Version]")
{
// clang-format off
auto versions = std::vector{
@ -517,7 +541,7 @@ namespace
* @see https://github.com/conda/conda/blob/main/tests/models/test_version.py
* @see https://github.com/pypa/packaging/blob/master/tests/test_version.py
*/
TEST_CASE("parse_pep440")
TEST_CASE("parse_pep440", "[mamba::specs][mamba::specs::Version]")
{
auto versions = std::vector{
// Implicit epoch of 0

View File

@ -18,7 +18,7 @@ namespace
using namespace mamba::specs::version_literals;
using namespace mamba::specs::version_spec_literals;
TEST_CASE("VersionPredicate")
TEST_CASE("VersionPredicate", "[mamba::specs][mamba::specs::VersionSpec]")
{
const auto v1 = "1.0"_v;
const auto v2 = "2.0"_v;
@ -32,6 +32,7 @@ namespace
REQUIRE(free.contains(v3));
REQUIRE(free.contains(v4));
REQUIRE(free.str() == "=*");
REQUIRE_FALSE(free.has_glob());
const auto eq = VersionPredicate::make_equal_to(v2);
REQUIRE_FALSE(eq.contains(v1));
@ -39,6 +40,7 @@ namespace
REQUIRE_FALSE(eq.contains(v3));
REQUIRE_FALSE(eq.contains(v4));
REQUIRE(eq.str() == "==2.0");
REQUIRE_FALSE(eq.has_glob());
const auto ne = VersionPredicate::make_not_equal_to(v2);
REQUIRE(ne.contains(v1));
@ -46,6 +48,7 @@ namespace
REQUIRE(ne.contains(v3));
REQUIRE(ne.contains(v4));
REQUIRE(ne.str() == "!=2.0");
REQUIRE_FALSE(ne.has_glob());
const auto gt = VersionPredicate::make_greater(v2);
REQUIRE_FALSE(gt.contains(v1));
@ -53,6 +56,7 @@ namespace
REQUIRE(gt.contains(v3));
REQUIRE(gt.contains(v4));
REQUIRE(gt.str() == ">2.0");
REQUIRE_FALSE(gt.has_glob());
const auto ge = VersionPredicate::make_greater_equal(v2);
REQUIRE_FALSE(ge.contains(v1));
@ -60,6 +64,7 @@ namespace
REQUIRE(ge.contains(v3));
REQUIRE(ge.contains(v4));
REQUIRE(ge.str() == ">=2.0");
REQUIRE_FALSE(ge.has_glob());
const auto lt = VersionPredicate::make_less(v2);
REQUIRE(lt.contains(v1));
@ -67,6 +72,7 @@ namespace
REQUIRE_FALSE(lt.contains(v3));
REQUIRE_FALSE(lt.contains(v4));
REQUIRE(lt.str() == "<2.0");
REQUIRE_FALSE(lt.has_glob());
const auto le = VersionPredicate::make_less_equal(v2);
REQUIRE(le.contains(v1));
@ -74,6 +80,7 @@ namespace
REQUIRE_FALSE(le.contains(v3));
REQUIRE_FALSE(le.contains(v4));
REQUIRE(le.str() == "<=2.0");
REQUIRE_FALSE(le.has_glob());
const auto sw = VersionPredicate::make_starts_with(v2);
REQUIRE_FALSE(sw.contains(v1));
@ -83,6 +90,7 @@ namespace
REQUIRE_FALSE(sw.contains(v4));
REQUIRE(sw.str() == "=2.0");
REQUIRE(sw.str_conda_build() == "2.0.*");
REQUIRE_FALSE(sw.has_glob());
const auto nsw = VersionPredicate::make_not_starts_with(v2);
REQUIRE(nsw.contains(v1));
@ -91,6 +99,7 @@ namespace
REQUIRE(nsw.contains(v3));
REQUIRE(nsw.contains(v4));
REQUIRE(nsw.str() == "!=2.0.*");
REQUIRE_FALSE(nsw.has_glob());
const auto cp2 = VersionPredicate::make_compatible_with(v2, 2);
REQUIRE_FALSE(cp2.contains(v1));
@ -99,6 +108,7 @@ namespace
REQUIRE_FALSE(cp2.contains(v3));
REQUIRE_FALSE(cp2.contains(v4));
REQUIRE(cp2.str() == "~=2.0");
REQUIRE_FALSE(cp2.has_glob());
const auto cp3 = VersionPredicate::make_compatible_with(v2, 3);
REQUIRE_FALSE(cp3.contains(v1));
@ -108,7 +118,76 @@ namespace
REQUIRE_FALSE(cp3.contains(v4));
REQUIRE(cp3.str() == "~=2.0.0");
const auto predicates = std::array{ free, eq, ne, lt, le, gt, ge, sw, cp2, cp3 };
const auto g1 = VersionPredicate::make_version_glob("*"_v);
REQUIRE(g1.contains(v1));
REQUIRE(g1.contains(v2));
REQUIRE(g1.contains(v201));
REQUIRE(g1.contains(v3));
REQUIRE(g1.contains(v4));
REQUIRE(g1.str() == "*");
REQUIRE(g1.has_glob());
const auto g2 = VersionPredicate::make_version_glob("*.0.*"_v);
REQUIRE_FALSE(g2.contains(v1));
REQUIRE_FALSE(g2.contains(v2));
REQUIRE(g2.contains(v201));
REQUIRE_FALSE(g2.contains(v3));
REQUIRE_FALSE(g2.contains(v4));
REQUIRE(g2.contains("1.0.1.1.1"_v));
REQUIRE(g2.str() == "*.0.*");
const auto g3 = VersionPredicate::make_version_glob("*.0"_v);
REQUIRE(g3.contains(v1));
REQUIRE(g3.contains(v2));
REQUIRE_FALSE(g3.contains(v201));
REQUIRE(g3.contains(v3));
REQUIRE(g3.contains(v4));
REQUIRE(g3.str() == "*.0");
const auto g4 = VersionPredicate::make_version_glob("2.*"_v);
REQUIRE_FALSE(g4.contains(v1));
REQUIRE(g4.contains(v2));
REQUIRE(g4.contains(v201));
REQUIRE_FALSE(g4.contains(v3));
REQUIRE_FALSE(g4.contains(v4));
REQUIRE(g4.str() == "2.*");
const auto g5 = VersionPredicate::make_version_glob("2.0"_v);
REQUIRE_FALSE(g5.contains(v1));
REQUIRE(g5.contains(v2));
REQUIRE_FALSE(g5.contains(v201));
REQUIRE_FALSE(g5.contains(v3));
REQUIRE_FALSE(g5.contains(v4));
REQUIRE(g5.str() == "2.0");
const auto g6 = VersionPredicate::make_version_glob("2.*.1"_v);
REQUIRE_FALSE(g6.contains(v1));
REQUIRE_FALSE(g6.contains(v2));
REQUIRE(g6.contains(v201));
REQUIRE_FALSE(g6.contains(v3));
REQUIRE_FALSE(g6.contains(v4));
REQUIRE(g6.str() == "2.*.1");
const auto g7 = VersionPredicate::make_version_glob("2.*.1.1.*"_v);
REQUIRE_FALSE(g7.contains(v1));
REQUIRE_FALSE(g7.contains(v2));
REQUIRE_FALSE(g7.contains(v201));
REQUIRE(g7.contains("2.0.1.0.1.1.3"_v));
REQUIRE(g7.str() == "2.*.1.1.*");
const auto ng1 = VersionPredicate::make_not_version_glob("2.*.1"_v);
REQUIRE(ng1.contains(v1));
REQUIRE(ng1.contains(v2));
REQUIRE_FALSE(ng1.contains(v201));
REQUIRE(ng1.contains(v3));
REQUIRE(ng1.contains(v4));
REQUIRE(ng1.str() == "!=2.*.1");
REQUIRE(ng1.has_glob());
const auto predicates = std::array{
free, eq, ne, lt, le, gt, ge, sw, cp2, cp3, g1, g2, g3, g4, g5, g6, g7, ng1,
};
REQUIRE("*.0"_v != "*.0.*"_v);
for (std::size_t i = 0; i < predicates.size(); ++i)
{
REQUIRE(predicates[i] == predicates[i]);
@ -119,7 +198,7 @@ namespace
}
}
TEST_CASE("Tree construction")
TEST_CASE("Tree construction", "[mamba::specs][mamba::specs::VersionSpec]")
{
SECTION("empty")
{
@ -172,7 +251,7 @@ namespace
}
}
TEST_CASE("VersionSpec::parse")
TEST_CASE("VersionSpec::parse", "[mamba::specs][mamba::specs::VersionSpec]")
{
SECTION("Successful")
{
@ -256,6 +335,24 @@ namespace
REQUIRE_FALSE(" != 1.8.*"_vs.contains("1.8alpha"_v)); // Like Conda
REQUIRE(" != 1.8.*"_vs.contains("1.9"_v));
REQUIRE(" 1.*.3"_vs.contains("1.7.3"_v));
REQUIRE(" 1.*.3"_vs.contains("1.7.0.3"_v));
REQUIRE_FALSE(" 1.*.3"_vs.contains("1.7.3.4"_v));
REQUIRE_FALSE(" 1.*.3"_vs.contains("1.3"_v));
REQUIRE_FALSE(" 1.*.3"_vs.contains("2.0.3"_v));
REQUIRE(" =1.*.3"_vs.contains("1.7.3"_v));
REQUIRE(" =1.*.3"_vs.contains("1.7.0.3"_v));
REQUIRE_FALSE(" =1.*.3"_vs.contains("1.7.3.4"_v));
REQUIRE_FALSE(" =1.*.3"_vs.contains("1.3"_v));
REQUIRE_FALSE(" =1.*.3"_vs.contains("2.0.3"_v));
REQUIRE_FALSE("!=1.*.3 "_vs.contains("1.7.3"_v));
REQUIRE_FALSE("!=1.*.3 "_vs.contains("1.7.0.3"_v));
REQUIRE("!=1.*.3 "_vs.contains("1.7.3.4"_v));
REQUIRE("!=1.*.3 "_vs.contains("1.3"_v));
REQUIRE("!=1.*.3 "_vs.contains("2.0.3"_v));
REQUIRE_FALSE(" ~= 1.8 "_vs.contains("1.7.0.1"_v));
REQUIRE(" ~= 1.8 "_vs.contains("1.8"_v));
REQUIRE(" ~= 1.8 "_vs.contains("1.8.0"_v));
@ -351,6 +448,17 @@ namespace
REQUIRE("~=3.3.2|==2.2"_vs.contains("2.2.0"_v));
REQUIRE("~=3.3.2|==2.2"_vs.contains("3.3.3"_v));
REQUIRE_FALSE("~=3.3.2|==2.2"_vs.contains("2.2.1"_v));
REQUIRE("*.*"_vs.contains("3.3"_v));
REQUIRE("*.*"_vs.contains("3.3.3"_v));
REQUIRE_FALSE("*.*"_vs.contains("3"_v));
REQUIRE("2.*.1.1.*"_vs.contains("2.0.1.0.1.1.3"_v));
REQUIRE_FALSE("2.*.1.1.*"_vs.contains("2.1.0.1.1"_v));
REQUIRE("*.3"_vs.contains("2.1.0.1.1.3"_v));
REQUIRE_FALSE("*.3"_vs.contains("0.3.4"_v));
REQUIRE("*.2023_10_12"_vs.contains("2.1.0.2023_10_12"_v));
REQUIRE(">=10.0,*.*"_vs.contains("10.1"_v));
REQUIRE_FALSE(">=10.0,*.*"_vs.contains("11"_v));
REQUIRE("1.*.1"_vs.contains("1.7.1"_v));
// Regex are currently not supported
// REQUIRE("^1.7.1$"_vs.contains("1.7.1"_v));
@ -367,7 +475,6 @@ namespace
// REQUIRE("1.6.*|^0.*$|1.7.1"_vs.contains("1.7.1"_v));
// REQUIRE("^0.*$|1.7.1"_vs.contains("1.7.1"_v));
// REQUIRE(R"(1.6.*|^.*\.7\.1$|0.7.1)"_vs.contains("1.7.1"_v));
// REQUIRE("1.*.1"_vs.contains("1.7.1"_v));
}
SECTION("Unsuccessful")
@ -407,7 +514,7 @@ namespace
}
}
TEST_CASE("VersionSpec::str")
TEST_CASE("VersionSpec::str", "[mamba::specs][mamba::specs::VersionSpec]")
{
SECTION("2.3")
{
@ -445,7 +552,7 @@ namespace
}
}
TEST_CASE("VersionSpec::is_explicitly_free")
TEST_CASE("VersionSpec::is_explicitly_free", "[mamba::specs][mamba::specs::VersionSpec]")
{
{
using namespace mamba::util;
@ -466,7 +573,17 @@ namespace
REQUIRE_FALSE(VersionSpec::parse("=2.3,<3.0").value().is_explicitly_free());
}
TEST_CASE("VersionSpec Comparability and hashability")
TEST_CASE("VersionSpec::has_glob", "[mamba::specs][mamba::specs::VersionSpec]")
{
REQUIRE(VersionSpec::parse("*.4").value().has_glob());
REQUIRE(VersionSpec::parse("1.*.0").value().has_glob());
REQUIRE(VersionSpec::parse("1.0|4.*.0").value().has_glob());
REQUIRE_FALSE(VersionSpec::parse("*").value().has_glob());
REQUIRE_FALSE(VersionSpec::parse("3.*").value().has_glob());
}
TEST_CASE("VersionSpec Comparability and hashability", "[mamba::specs][mamba::specs::VersionSpec]")
{
auto spec1 = VersionSpec::parse("*").value();
auto spec2 = VersionSpec::parse("*").value();

View File

@ -608,7 +608,7 @@ namespace mambapy
.def_readonly_static("less_equal_str", &VersionSpec::less_equal_str)
.def_readonly_static("compatible_str", &VersionSpec::compatible_str)
.def_readonly_static("glob_suffix_str", &VersionSpec::glob_suffix_str)
.def_readonly_static("glob_suffix_token", &VersionSpec::glob_suffix_token)
.def_readonly_static("glob_suffix_token", &VersionSpec::glob_suffix_str.back())
.def_static("parse", &VersionSpec::parse, py::arg("str"))
.def_static("from_predicate", &VersionSpec::from_predicate, py::arg("pred"))
.def(py::init<>())