MatchSpec use VersionSpec (#3089)

* Don't use members as wip vars

* Make conda-build compatible VersionSpec str

* Add VersionSpec::is_explicitly_free

* Strenghten sat attrs ref

* Try pluging VersionSpec in MatchSpec

* Change type of MatchSpec::version

* Fix MatchSpec::conda_build_str

* Adjust MatchSpec tests
This commit is contained in:
Antoine Prouvost 2024-01-02 12:55:42 +01:00 committed by GitHub
parent 108bc9f5a4
commit b9706dce32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 212 additions and 143 deletions

View File

@ -14,6 +14,7 @@
#include <unordered_map>
#include "mamba/specs/channel_spec.hpp"
#include "mamba/specs/version_spec.hpp"
namespace mamba::specs
{
@ -37,8 +38,8 @@ namespace mamba::specs
[[nodiscard]] auto name() const -> const std::string&;
void set_name(std::string name);
[[nodiscard]] auto version() const -> const std::string&;
void set_version(std::string ver);
[[nodiscard]] auto version() const -> const VersionSpec&;
void set_version(VersionSpec ver);
[[nodiscard]] auto build_number() const -> const std::string&;
void set_build_number(std::string num);
@ -67,9 +68,9 @@ namespace mamba::specs
private:
std::optional<ChannelSpec> m_channel;
VersionSpec m_version;
std::string m_name_space;
std::string m_name;
std::string m_version;
std::string m_build_number;
std::string m_build_string;
// TODO can put inside channel spec

View File

@ -48,6 +48,13 @@ namespace mamba::specs
[[nodiscard]] auto str() const -> std::string;
/**
* An alternative string representation of the version spec.
*
* Attempts to be compatible with conda-build/libsolv.
*/
[[nodiscard]] auto str_conda_build() const -> std::string;
private:
struct free_interval
@ -148,6 +155,15 @@ namespace mamba::specs
VersionSpec() = default;
explicit VersionSpec(tree_type&& tree) noexcept;
/**
* Returns wether the VersionSpec is unconstrained.
*
* Due to the complex nature of VersionSpec expressions, it is not always easy to know
* whether a complex expression can be simpified to the unconstrained one.
* This functions only handles the trivial cases.
*/
[[nodiscard]] auto is_explicitly_free() const -> bool;
/**
* A string representation of the version spec.
*
@ -156,6 +172,13 @@ namespace mamba::specs
*/
[[nodiscard]] auto str() const -> std::string;
/**
* An alternative string representation of the version spec.
*
* Attempts to be compatible with conda-build/libsolv.
*/
[[nodiscard]] auto str_conda_build() const -> std::string;
/**
* True if the set described by the VersionSpec contains the given version.
*/
@ -177,6 +200,11 @@ namespace mamba::specs
template <>
struct fmt::formatter<mamba::specs::VersionPredicate>
{
/**
* Change the representation of some predicates not understood by conda-build/libsolv.
*/
bool conda_build_form = false;
auto parse(format_parse_context& ctx) -> decltype(ctx.begin());
auto format(const ::mamba::specs::VersionPredicate& pred, format_context& ctx)
@ -186,6 +214,11 @@ struct fmt::formatter<mamba::specs::VersionPredicate>
template <>
struct fmt::formatter<mamba::specs::VersionSpec>
{
/**
* Change the representation of some predicates not understood by conda-build/libsolv.
*/
bool conda_build_form = false;
auto parse(format_parse_context& ctx) -> decltype(ctx.begin());
auto format(const ::mamba::specs::VersionSpec& spec, format_context& ctx) -> decltype(ctx.out());

View File

@ -358,7 +358,7 @@ namespace mamba
PackageInfo p(ms.name());
p.url = ms.url();
p.build_string = ms.build_string();
p.version = ms.version();
p.version = ms.version().str();
if (ms.channel().has_value())
{
p.channel = ms.channel()->location();

View File

@ -148,7 +148,7 @@ namespace mamba
std::vector<old_node_id_list> groups{};
std::size_t const n_nodes = node_indices.size();
const std::size_t n_nodes = node_indices.size();
std::vector<bool> node_added_to_a_group(n_nodes, false);
for (std::size_t i = 0; i < n_nodes; ++i)
{
@ -538,14 +538,45 @@ namespace mamba
* Implementation of CompressedProblemsGraph::RoughCompare *
*************************************************************/
namespace
{
template <typename T>
auto invoke_name(T&& e) -> decltype(auto)
{
using TT = std::remove_cv_t<std::remove_reference_t<T>>;
return std::invoke(&TT::name, std::forward<T>(e));
}
template <typename T>
auto invoke_version(T&& e) -> decltype(auto)
{
using TT = std::remove_cv_t<std::remove_reference_t<T>>;
using Ver = decltype(std::invoke(&TT::version, std::forward<T>(e)));
Ver v = std::invoke(&TT::version, std::forward<T>(e));
if constexpr (std::is_same_v<std::decay_t<decltype(v)>, specs::VersionSpec>)
{
return std::forward<Ver>(v).str();
}
else
{
return v;
}
}
}
template <typename T>
bool CompressedProblemsGraph::RoughCompare<T>::operator()(const T& a, const T& b) const
auto CompressedProblemsGraph::RoughCompare<T>::operator()(const T& a, const T& b) const -> bool
{
auto attrs = [](const auto& x)
{
return std::tie(
std::invoke(&T::name, x),
std::invoke(&T::version, x),
using Attrs = std::tuple<
decltype(invoke_name(x)),
decltype(invoke_version(x)),
decltype(std::invoke(&T::build_number, x)),
decltype(std::invoke(&T::build_string, x))>;
return Attrs(
invoke_name(x),
invoke_version(x),
std::invoke(&T::build_number, x),
std::invoke(&T::build_string, x)
);
@ -562,16 +593,6 @@ namespace mamba
* Implementation of CompressedProblemsGraph::NamedList *
**********************************************************/
namespace
{
template <typename T>
decltype(auto) invoke_name(T&& e)
{
using TT = std::remove_cv_t<std::remove_reference_t<T>>;
return std::invoke(&TT::name, std::forward<T>(e));
}
}
template <typename T, typename A>
template <typename InputIterator>
CompressedProblemsGraph::NamedList<T, A>::NamedList(InputIterator first, InputIterator last)
@ -650,13 +671,13 @@ namespace mamba
) const -> std::pair<std::string, std::size_t>
{
auto versions = std::vector<std::string>(size());
auto invoke_version = [](auto&& v) -> decltype(auto)
{
using TT = std::remove_cv_t<std::remove_reference_t<decltype(v)>>;
return std::invoke(&TT::version, std::forward<decltype(v)>(v));
};
// TODO(C++20) *this | std::ranges::transform(invoke_version) | ranges::unique
std::transform(begin(), end(), versions.begin(), invoke_version);
std::transform(
begin(),
end(),
versions.begin(),
[](const auto& x) { return invoke_version(x); }
);
if (remove_duplicates)
{
versions.erase(std::unique(versions.begin(), versions.end()), versions.end());
@ -1036,7 +1057,7 @@ namespace mamba
const TreeNodeIter children_begin = out;
// TODO(C++20) an enumerate view ``views::zip(views::iota(), children_ids)``
std::size_t const n_children = children_ids.size();
const std::size_t n_children = children_ids.size();
for (std::size_t i = 0; i < n_children; ++i)
{
const bool last = (i == n_children - 1);
@ -1228,7 +1249,7 @@ namespace mamba
void TreeExplainer::write_ancestry(const std::vector<SiblingNumber>& ancestry)
{
std::size_t const size = ancestry.size();
const std::size_t size = ancestry.size();
const auto indents = m_format.indents;
if (size > 0)
{
@ -1434,7 +1455,7 @@ namespace mamba
void TreeExplainer::write_path(const std::vector<TreeNode>& path)
{
std::size_t const length = path.size();
const std::size_t length = path.size();
for (std::size_t i = 0; i < length; ++i)
{
const bool last = (i == length - 1);

View File

@ -96,7 +96,8 @@ namespace mamba
return m_jobs->push_back(job_flag | SOLVER_SOLVABLE_PROVIDES, m_pool.matchspec2id(ms));
}
if (ms.channel().has_value() || !ms.version().empty() || !ms.build_string().empty())
if (ms.channel().has_value() || !ms.version().is_explicitly_free()
|| !ms.build_string().empty())
{
Console::stream() << ms.conda_build_form()
<< ": overriding channel, version and build from "
@ -105,7 +106,7 @@ namespace mamba
auto ms_modified = ms;
ms_modified.set_channel(specs::ChannelSpec::parse(solvable->channel()));
ms_modified.set_version(std::string(solvable->version()));
ms_modified.set_version(specs::VersionSpec::parse(solvable->version()));
ms_modified.set_build_string(std::string(solvable->build_string()));
LOG_INFO << "Reinstall " << ms_modified.conda_build_form() << " from channel "

View File

@ -73,7 +73,7 @@ namespace mamba
auto& p = out.back();
p.url = ms.url();
p.build_string = ms.build_string();
p.version = ms.version();
p.version = ms.version().str_conda_build();
if (ms.channel().has_value())
{
p.channel = ms.channel()->location();

View File

@ -77,7 +77,7 @@ namespace mamba::specs
// Version
std::tie(head, tail) = util::rsplit_once(head.value(), '-');
out.m_version = tail;
out.m_version = VersionSpec::parse(tail);
if (!head.has_value())
{
fail_parse();
@ -192,12 +192,14 @@ namespace mamba::specs
spec_str.push_back('*');
}
// This is #6 of the spec parsing
// Look for version *and* build string and separator
auto version_and_build = std::string();
static std::regex version_build_re("([^ =<>!~]+)?([><!=~ ].+)?");
std::smatch vb_match;
if (std::regex_match(spec_str, vb_match, version_build_re))
{
out.m_name = vb_match[1].str();
out.m_version = util::strip(vb_match[2].str());
version_and_build = util::strip(vb_match[2].str());
if (out.m_name.size() == 0)
{
throw std::runtime_error("Invalid spec, no package name found: " + spec_str);
@ -210,9 +212,9 @@ namespace mamba::specs
// # Step 7. otherwise sort out version + build
// spec_str = spec_str and spec_str.strip()
if (!out.m_version.empty())
if (!version_and_build.empty())
{
if (out.m_version.find('[') != out.m_version.npos)
if (version_and_build.find('[') != std::string::npos)
{
throw std::runtime_error(util::concat(
R"(Invalid match spec: multiple bracket sections not allowed ")",
@ -221,38 +223,15 @@ namespace mamba::specs
));
}
out.m_version = std::string(util::strip(out.m_version));
auto [pv, pb] = parse_version_and_build(std::string(util::strip(out.m_version)));
auto [pv, pb] = parse_version_and_build(version_and_build);
out.m_version = pv;
out.m_version = VersionSpec::parse(pv);
out.m_build_string = pb;
// translate version '=1.2.3' to '1.2.3*'
// is it a simple version starting with '='? i.e. '=1.2.3'
if (out.m_version.size() >= 2 && out.m_version[0] == '=')
{
auto rest = out.m_version.substr(1);
if (out.m_version[1] == '=' && out.m_build_string.empty())
{
out.m_version = out.m_version.substr(2);
}
else if (rest.find_first_of("=,|") == rest.npos)
{
if (out.m_build_string.empty() && out.m_version.back() != '*')
{
out.m_version = util::concat(out.m_version, "*");
}
else
{
out.m_version = rest;
}
}
}
}
else
else // no-op
{
out.m_version = "";
out.m_build_string = "";
out.m_version = {};
out.m_build_string = {};
}
// TODO think about using a hash function here, (and elsewhere), like:
@ -270,7 +249,7 @@ namespace mamba::specs
}
else if (k == "version")
{
out.m_version = v;
out.m_version = VersionSpec::parse(v);
}
else if (k == "channel")
{
@ -351,12 +330,12 @@ namespace mamba::specs
m_name = std::move(name);
}
auto MatchSpec::version() const -> const std::string&
auto MatchSpec::version() const -> const VersionSpec&
{
return m_version;
}
void MatchSpec::set_version(std::string ver)
void MatchSpec::set_version(VersionSpec ver)
{
m_version = std::move(ver);
}
@ -405,14 +384,17 @@ namespace mamba::specs
{
std::stringstream res;
res << m_name;
if (!m_version.empty())
if (!m_version.is_explicitly_free())
{
res << " " << m_version;
// if (!build.empty() && (build != "*"))
if (!m_build_string.empty())
{
res << " " << m_build_string;
}
res << " " << m_version.str_conda_build();
}
else if (!m_build_string.empty())
{
res << " *";
}
if (!m_build_string.empty())
{
res << " " << m_build_string;
}
return res.str();
}
@ -453,50 +435,16 @@ namespace mamba::specs
auto is_complex_relation = [](const std::string& s)
{ return s.find_first_of("><$^|,") != s.npos; };
if (!m_version.empty())
if (!m_version.is_explicitly_free())
{
if (is_complex_relation(m_version))
auto ver = m_version.str();
if (is_complex_relation(ver)) // TODO do on VersionSpec
{
formatted_brackets.push_back(util::concat("version='", m_version, "'"));
}
else if (util::starts_with(m_version, "!=") || util::starts_with(m_version, "~="))
{
if (!m_build_string.empty())
{
formatted_brackets.push_back(util::concat("version='", m_version, "'"));
}
else
{
res << " " << m_version;
}
}
else if (util::ends_with(m_version, ".*"))
{
res << "=" + m_version.substr(0, m_version.size() - 2);
}
else if (m_version.back() == '*')
{
if (m_version.size() == 1)
{
res << "=*";
}
else if (util::starts_with(m_version, "="))
{
res << m_version.substr(0, m_version.size() - 1);
}
else
{
res << "=" + m_version.substr(0, m_version.size() - 1);
}
}
else if (util::starts_with(m_version, "=="))
{
res << m_version;
version_exact = true;
formatted_brackets.push_back(util::concat("version='", ver, "'"));
}
else
{
res << "==" << m_version;
res << ver;
version_exact = true;
}
}
@ -566,7 +514,7 @@ namespace mamba::specs
auto MatchSpec::is_simple() const -> bool
{
return m_version.empty() && m_build_string.empty() && m_build_number.empty();
return m_version.is_explicitly_free() && m_build_string.empty() && m_build_number.empty();
}
auto MatchSpec::is_file() const -> bool

View File

@ -160,6 +160,11 @@ namespace mamba::specs
return fmt::format("{}", *this);
}
auto VersionPredicate::str_conda_build() const -> std::string
{
return fmt::format("{:b}", *this);
}
VersionPredicate::VersionPredicate(Version ver, BinaryOperator op)
: m_version(std::move(ver))
, m_operator(std::move(op))
@ -181,10 +186,10 @@ auto
fmt::formatter<mamba::specs::VersionPredicate>::parse(format_parse_context& ctx)
-> decltype(ctx.begin())
{
// make sure that range is empty
if (ctx.begin() != ctx.end() && *ctx.begin() != '}')
if (auto it = std::find(ctx.begin(), ctx.end(), 'b'); it < ctx.end())
{
throw fmt::format_error("Invalid format");
conda_build_form = true;
return ++it;
}
return ctx.begin();
}
@ -239,7 +244,14 @@ fmt::formatter<mamba::specs::VersionPredicate>::format(
}
if constexpr (std::is_same_v<Op, VersionPredicate::starts_with>)
{
out = fmt::format_to(out, "{}{}", VersionSpec::starts_with_str, pred.m_version);
if (conda_build_form)
{
out = fmt::format_to(out, "{}{}", pred.m_version, VersionSpec::glob_suffix_str);
}
else
{
out = fmt::format_to(out, "{}{}", VersionSpec::starts_with_str, pred.m_version);
}
}
if constexpr (std::is_same_v<Op, VersionPredicate::not_starts_with>)
{
@ -283,11 +295,23 @@ namespace mamba::specs
return m_tree.evaluate([&point](const auto& node) { return node.contains(point); });
}
auto VersionSpec::is_explicitly_free() const -> bool
{
const auto free_pred = VersionPredicate::make_free();
const auto is_free_pred = [&free_pred](const auto& node) { return node == free_pred; };
return m_tree.empty() || ((m_tree.size() == 1) && m_tree.evaluate(is_free_pred));
}
auto VersionSpec::str() const -> std::string
{
return fmt::format("{}", *this);
}
auto VersionSpec::str_conda_build() const -> std::string
{
return fmt::format("{:b}", *this);
}
namespace
{
template <typename Val, typename Range>
@ -464,10 +488,10 @@ namespace mamba::specs
auto
fmt::formatter<mamba::specs::VersionSpec>::parse(format_parse_context& ctx) -> decltype(ctx.begin())
{
// make sure that range is empty
if (ctx.begin() != ctx.end() && *ctx.begin() != '}')
if (auto it = std::find(ctx.begin(), ctx.end(), 'b'); it < ctx.end())
{
throw fmt::format_error("Invalid format");
conda_build_form = true;
return ++it;
}
return ctx.begin();
}
@ -511,7 +535,7 @@ fmt::formatter<mamba::specs::VersionSpec>::format(
}
if constexpr (std::is_same_v<Token, tree_type::variable_type>)
{
out = fmt::format_to(out, "{}", token);
out = fmt::format_to(out, conda_build_form ? "{:b}" : "{}", token);
}
}
);

View File

@ -67,29 +67,29 @@ TEST_SUITE("specs::match_spec")
{
{
auto ms = MatchSpec::parse("xtensor==0.12.3");
CHECK_EQ(ms.version(), "0.12.3");
CHECK_EQ(ms.version().str(), "==0.12.3");
CHECK_EQ(ms.name(), "xtensor");
}
{
auto ms = MatchSpec::parse("");
CHECK_EQ(ms.version(), "");
CHECK_EQ(ms.version().str(), "=*");
CHECK_EQ(ms.name(), "");
}
{
auto ms = MatchSpec::parse("ipykernel");
CHECK_EQ(ms.version(), "");
CHECK_EQ(ms.version().str(), "=*");
CHECK_EQ(ms.name(), "ipykernel");
}
{
auto ms = MatchSpec::parse("ipykernel ");
CHECK_EQ(ms.version(), "");
CHECK_EQ(ms.version().str(), "=*");
CHECK_EQ(ms.name(), "ipykernel");
}
{
auto ms = MatchSpec::parse("numpy 1.7*");
CHECK_EQ(ms.version(), "1.7*");
CHECK_EQ(ms.version().str(), "=1.7");
CHECK_EQ(ms.name(), "numpy");
CHECK_EQ(ms.conda_build_form(), "numpy 1.7*");
CHECK_EQ(ms.conda_build_form(), "numpy 1.7.*");
CHECK_EQ(ms.str(), "numpy=1.7");
}
{
@ -98,11 +98,11 @@ TEST_SUITE("specs::match_spec")
// CHECK_EQ(ms.version, "1.7|1.8");
CHECK_EQ(ms.name(), "numpy");
CHECK_EQ(ms.brackets["version"], "1.7|1.8");
CHECK_EQ(ms.str(), "numpy[version='1.7|1.8']");
CHECK_EQ(ms.str(), "numpy[version='==1.7|==1.8']");
}
{
auto ms = MatchSpec::parse("conda-forge/linux-64::xtensor==0.12.3");
CHECK_EQ(ms.version(), "0.12.3");
CHECK_EQ(ms.version().str(), "==0.12.3");
CHECK_EQ(ms.name(), "xtensor");
REQUIRE(ms.channel().has_value());
CHECK_EQ(ms.channel()->location(), "conda-forge");
@ -111,7 +111,7 @@ TEST_SUITE("specs::match_spec")
}
{
auto ms = MatchSpec::parse("conda-forge::foo[build=3](target=blarg,optional)");
CHECK_EQ(ms.version(), "");
CHECK_EQ(ms.version().str(), "=*");
CHECK_EQ(ms.name(), "foo");
REQUIRE(ms.channel().has_value());
CHECK_EQ(ms.channel()->location(), "conda-forge");
@ -136,7 +136,7 @@ TEST_SUITE("specs::match_spec")
"https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2"
);
CHECK_EQ(ms.name(), "_libgcc_mutex");
CHECK_EQ(ms.version(), "0.1");
CHECK_EQ(ms.version().str(), "==0.1");
CHECK_EQ(ms.build_string(), "conda_forge");
CHECK_EQ(
ms.url(),
@ -149,7 +149,7 @@ TEST_SUITE("specs::match_spec")
"https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-11.2.0-h1d223b6_13.tar.bz2"
);
CHECK_EQ(ms.name(), "libgcc-ng");
CHECK_EQ(ms.version(), "11.2.0");
CHECK_EQ(ms.version().str(), "==11.2.0");
CHECK_EQ(ms.build_string(), "h1d223b6_13");
CHECK_EQ(
ms.url(),
@ -162,7 +162,7 @@ TEST_SUITE("specs::match_spec")
"/home/randomguy/Downloads/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2"
);
CHECK_EQ(ms.name(), "_libgcc_mutex");
CHECK_EQ(ms.version(), "0.1");
CHECK_EQ(ms.version().str(), "==0.1");
CHECK_EQ(ms.build_string(), "conda_forge");
if (util::on_win)
{
@ -199,21 +199,21 @@ TEST_SUITE("specs::match_spec")
}
{
auto ms = MatchSpec::parse("foo=1.0=2");
CHECK_EQ(ms.conda_build_form(), "foo 1.0 2");
CHECK_EQ(ms.str(), "foo==1.0=2");
CHECK_EQ(ms.conda_build_form(), "foo 1.0.* 2");
CHECK_EQ(ms.str(), "foo=1.0=2");
}
{
auto ms = MatchSpec::parse("foo=1.0=2[md5=123123123, license=BSD-3, fn='test 123.tar.bz2']"
);
CHECK_EQ(ms.conda_build_form(), "foo 1.0 2");
CHECK_EQ(ms.str(), "foo==1.0=2[md5=123123123,license=BSD-3,fn='test 123.tar.bz2']");
CHECK_EQ(ms.conda_build_form(), "foo 1.0.* 2");
CHECK_EQ(ms.str(), "foo=1.0=2[md5=123123123,license=BSD-3,fn='test 123.tar.bz2']");
}
{
auto ms = MatchSpec::parse(
"foo=1.0=2[md5=123123123, license=BSD-3, fn='test 123.tar.bz2', url='abcdef']"
);
CHECK_EQ(ms.conda_build_form(), "foo 1.0 2");
CHECK_EQ(ms.str(), "foo==1.0=2[url=abcdef,md5=123123123,license=BSD-3]");
CHECK_EQ(ms.conda_build_form(), "foo 1.0.* 2");
CHECK_EQ(ms.str(), "foo=1.0=2[url=abcdef,md5=123123123,license=BSD-3]");
}
{
auto ms = MatchSpec::parse("libblas=*=*mkl");
@ -222,12 +222,14 @@ TEST_SUITE("specs::match_spec")
}
{
auto ms = MatchSpec::parse("libblas=0.15*");
CHECK_EQ(ms.conda_build_form(), "libblas 0.15*");
CHECK_EQ(ms.conda_build_form(), "libblas 0.15*.*");
}
{
// '*' is part of the version, not the glob
auto ms = MatchSpec::parse("xtensor =0.15*");
CHECK_EQ(ms.conda_build_form(), "xtensor 0.15*");
CHECK_EQ(ms.str(), "xtensor=0.15");
CHECK_EQ(ms.conda_build_form(), "xtensor 0.15*.*");
CHECK_EQ(ms.str(), "xtensor=0.15*");
CHECK_EQ(ms.version().str(), "=0.15*");
}
{
auto ms = MatchSpec::parse("numpy=1.20");

View File

@ -80,6 +80,7 @@ TEST_SUITE("specs::version_spec")
CHECK_FALSE(sw.contains(v3));
CHECK_FALSE(sw.contains(v4));
CHECK_EQ(sw.str(), "=2.0");
CHECK_EQ(sw.str_conda_build(), "2.0.*");
const auto nsw = VersionPredicate::make_not_starts_with(v2);
CHECK(nsw.contains(v1));
@ -397,4 +398,42 @@ TEST_SUITE("specs::version_spec")
}
}
}
TEST_CASE("VersionSpec::str")
{
SUBCASE("2.3")
{
auto vs = VersionSpec::parse("2.3");
CHECK_EQ(vs.str(), "==2.3");
CHECK_EQ(vs.str_conda_build(), "==2.3");
}
SUBCASE("=2.3,<3.0")
{
auto vs = VersionSpec::parse("=2.3,<3.0");
CHECK_EQ(vs.str(), "=2.3,<3.0");
CHECK_EQ(vs.str_conda_build(), "2.3.*,<3.0");
}
}
TEST_CASE("VersionSpec::is_free")
{
{
using namespace mamba::util;
auto parser = InfixParser<VersionPredicate, BoolOperator>{};
parser.push_variable(VersionPredicate::make_free());
parser.finalize();
auto spec = VersionSpec(std::move(parser).tree());
CHECK(spec.is_explicitly_free());
}
CHECK(VersionSpec().is_explicitly_free());
CHECK(VersionSpec::parse("*").is_explicitly_free());
CHECK(VersionSpec::parse("").is_explicitly_free());
CHECK_FALSE(VersionSpec::parse("==2.3|!=2.3").is_explicitly_free());
CHECK_FALSE(VersionSpec::parse("=2.3,<3.0").is_explicitly_free());
}
}

View File

@ -666,4 +666,4 @@ def test_MatchSpec():
ms = MatchSpec.parse("conda-forge::python=3.7=*pypy")
# str
assert str(ms) == "conda-forge::python==3.7[build=*pypy]"
assert str(ms) == "conda-forge::python=3.7[build=*pypy]"