Skip to content

Commit

Permalink
Add some PackageInfo tests" (#3115)
Browse files Browse the repository at this point in the history
* Add PackageInfo::from_url

* Add PakcageInfo tests

* Bind PackageInfo::from_url
  • Loading branch information
AntoinePrv authored Jan 9, 2024
1 parent 6bf43a6 commit 3284572
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 8 deletions.
3 changes: 3 additions & 0 deletions libmamba/include/mamba/specs/package_info.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#define MAMBA_CORE_PACKAGE_INFO

#include <string>
#include <string_view>
#include <vector>

#include <nlohmann/json_fwd.hpp>
Expand Down Expand Up @@ -45,6 +46,8 @@ namespace mamba::specs
std::size_t size = 0;
std::size_t timestamp = 0;

[[nodiscard]] static auto from_url(std::string_view url) -> PackageInfo;

PackageInfo() = default;
explicit PackageInfo(std::string name);
PackageInfo(std::string name, std::string version, std::string build_string, std::size_t build_number);
Expand Down
109 changes: 101 additions & 8 deletions libmamba/src/specs/package_info.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,91 @@
#include "mamba/specs/archive.hpp"
#include "mamba/specs/package_info.hpp"
#include "mamba/util/string.hpp"
#include "mamba/util/url_manip.hpp"

namespace mamba::specs
{
namespace
{
auto parse_url(std::string_view spec) -> PackageInfo
{
auto fail_parse = [&]() {
throw std::invalid_argument(
util::concat(R"(Fail to parse PackageInfo URL ")", spec, '"')
);
};

auto out = PackageInfo();
auto [_, pkg] = util::rsplit_once(spec, '/');
out.filename = pkg;
out.package_url = util::path_or_url_to_url(spec);

// Build string
auto [head, tail] = util::rsplit_once(strip_archive_extension(pkg), '-');
out.build_string = tail;
if (!head.has_value())
{
fail_parse();
}

// Version
std::tie(head, tail) = util::rsplit_once(head.value(), '-');
out.version = tail;
if (!head.has_value())
{
fail_parse();
}

// Name
out.name = head.value(); // There may be '-' in the name

return out;
}

auto is_hash(std::string_view text) -> bool
{
constexpr auto is_hash_char = [](char c) -> bool
{
auto const lower = util::to_lower(c);
return util::is_digit(c) || (lower == 'a') || (lower == 'b') || (lower == 'c')
|| (lower == 'd') || (lower == 'e') || (lower == 'f');
};
return std::all_of(text.cbegin(), text.cend(), is_hash_char);
}
}

auto PackageInfo::from_url(std::string_view str) -> PackageInfo
{
str = util::strip(str);
if (str.empty())
{
return {};
}

// A plain URL like https://conda.anaconda.org/conda-forge/linux-64/pkg-6.4-bld.conda
if (has_archive_extension(str))
{
return parse_url(str);
}

// A URL with hash, generated by `mamba env export --explicit` like
// https://conda.anaconda.org/conda-forge/linux-64/pkg-6.4-bld.conda#7dbaa197d7ba6032caf7ae7f32c1efa0
if (const auto idx = str.rfind('#'); idx != std::string_view::npos)
{
auto url = str.substr(0, idx);
auto hash = str.substr(idx + 1);
if (has_archive_extension(url))
{
auto out = parse_url(url);
if (is_hash(hash))
{
out.md5 = hash;
}
return out;
}
}
throw std::invalid_argument(util::concat(R"(Fail to parse PackageInfo URL ")", str, '"'));
}

PackageInfo::PackageInfo(std::string n)
: name(std::move(n))
Expand Down Expand Up @@ -156,13 +238,13 @@ namespace mamba::specs
}
if (field_name == "noarch")
{
return invoke_field_string(*this, &PackageInfo::noarch);
return std::string(noarch_name(noarch));
}
if (field_name == "channel")
{
return invoke_field_string(*this, &PackageInfo::channel);
}
if (field_name == "url")
if (field_name == "package_url" || field_name == "url")
{
return invoke_field_string(*this, &PackageInfo::package_url);
}
Expand Down Expand Up @@ -232,9 +314,9 @@ namespace mamba::specs
j["name"] = pkg.name;
j["version"] = pkg.version;
j["channel"] = pkg.channel;
j["url"] = pkg.package_url;
j["url"] = pkg.package_url; // The conda key name
j["subdir"] = pkg.subdir;
j["fn"] = pkg.filename;
j["fn"] = pkg.filename; // The conda key name
j["size"] = pkg.size;
j["timestamp"] = pkg.timestamp;
j["build"] = pkg.build_string;
Expand All @@ -245,7 +327,7 @@ namespace mamba::specs
j["noarch"] = pkg.noarch;
}
j["license"] = pkg.license;
j["track_features"] = fmt::format("{}", fmt::join(pkg.track_features, ","));
j["track_features"] = fmt::format("{}", fmt::join(pkg.track_features, ",")); // Conda fmt
if (!pkg.md5.empty())
{
j["md5"] = pkg.md5;
Expand Down Expand Up @@ -295,10 +377,21 @@ namespace mamba::specs
pkg.license = j.value("license", "");
pkg.md5 = j.value("md5", "");
pkg.sha256 = j.value("sha256", "");
if (std::string feat = j.value("track_features", ""); !feat.empty())
if (auto it = j.find("track_features"); it != j.end())
{
// Split empty string would have an empty element
pkg.track_features = util::split(feat, ",");
if (it->is_string() && !it->get<std::string_view>().empty())
{
// Split empty string would have an empty element
pkg.track_features = util::split(it->get<std::string_view>(), ",");
}
if (it->is_array())
{
pkg.track_features.reserve(it->size());
for (const auto& elem : *it)
{
pkg.track_features.emplace_back(elem);
}
}
}

// add the noarch type if we know it (only known for installed packages)
Expand Down
1 change: 1 addition & 0 deletions libmamba/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ set(
src/specs/test_conda_url.cpp
src/specs/test_glob_spec.cpp
src/specs/test_match_spec.cpp
src/specs/test_package_info.cpp
src/specs/test_platform.cpp
src/specs/test_repo_data.cpp
src/specs/test_version.cpp
Expand Down
168 changes: 168 additions & 0 deletions libmamba/tests/src/specs/test_package_info.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// Copyright (c) 2023, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.

#include <string>
#include <vector>

#include <doctest/doctest.h>
#include <nlohmann/json.hpp>

#include "mamba/specs/package_info.hpp"

#include "doctest-printer/vector.hpp"

namespace nl = nlohmann;
using namespace mamba::specs;

TEST_SUITE("specs::package_info")
{
TEST_CASE("PackageInfo::from_url")
{
SUBCASE("https://conda.anaconda.org/conda-forge/linux-64/pkg-6.4-bld.conda")
{
static constexpr std::string_view url = "https://conda.anaconda.org/conda-forge/linux-64/pkg-6.4-bld.conda";

auto pkg = PackageInfo::from_url(url);

CHECK_EQ(pkg.name, "pkg");
CHECK_EQ(pkg.version, "6.4");
CHECK_EQ(pkg.build_string, "bld");
CHECK_EQ(pkg.filename, "pkg-6.4-bld.conda");
CHECK_EQ(pkg.package_url, url);
CHECK_EQ(pkg.md5, "");
}

SUBCASE("https://conda.anaconda.org/conda-forge/linux-64/pkg-6.4-bld.conda#7dbaa197d7ba6032caf7ae7f32c1efa0"
)
{
static constexpr std::string_view url = "https://conda.anaconda.org/conda-forge/linux-64/pkg-6.4-bld.conda#7dbaa197d7ba6032caf7ae7f32c1efa0";

auto pkg = PackageInfo::from_url(url);

CHECK_EQ(pkg.name, "pkg");
CHECK_EQ(pkg.version, "6.4");
CHECK_EQ(pkg.build_string, "bld");
CHECK_EQ(pkg.filename, "pkg-6.4-bld.conda");
CHECK_EQ(pkg.package_url, url.substr(0, url.rfind('#')));
CHECK_EQ(pkg.md5, url.substr(url.rfind('#') + 1));
}
}

TEST_CASE("PackageInfo serialization")
{
using StrVec = std::vector<std::string>;

auto pkg = PackageInfo();
pkg.name = "foo";
pkg.version = "4.0";
pkg.build_string = "mybld";
pkg.build_number = 5;
pkg.noarch = NoArchType::Generic;
pkg.channel = "conda-forge";
pkg.package_url = "https://repo.mamba.pm/conda-forge/linux-64/foo-4.0-mybld.conda";
pkg.subdir = "linux-64";
pkg.filename = "foo-4.0-mybld.conda";
pkg.license = "MIT";
pkg.size = 3200;
pkg.timestamp = 4532;
pkg.sha256 = "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b";
pkg.md5 = "68b329da9893e34099c7d8ad5cb9c940";
pkg.track_features = { "mkl", "blas" };
pkg.depends = { "python>=3.7", "requests" };
pkg.constrains = { "pip>=2.1" };

SUBCASE("field")
{
CHECK_EQ(pkg.field("name"), "foo");
CHECK_EQ(pkg.field("version"), "4.0");
CHECK_EQ(pkg.field("build_string"), "mybld");
CHECK_EQ(pkg.field("build_number"), "5");
CHECK_EQ(pkg.field("noarch"), "generic");
CHECK_EQ(pkg.field("channel"), "conda-forge");
CHECK_EQ(
pkg.field("package_url"),
"https://repo.mamba.pm/conda-forge/linux-64/foo-4.0-mybld.conda"
);
CHECK_EQ(pkg.field("subdir"), "linux-64");
CHECK_EQ(pkg.field("filename"), "foo-4.0-mybld.conda");
CHECK_EQ(pkg.field("license"), "MIT");
CHECK_EQ(pkg.field("size"), "3200");
CHECK_EQ(pkg.field("timestamp"), "4532");
}

SUBCASE("to_json")
{
const auto j = nl::json(pkg);
CHECK_EQ(j.at("name"), "foo");
CHECK_EQ(j.at("version"), "4.0");
CHECK_EQ(j.at("build_string"), "mybld");
CHECK_EQ(j.at("build_number"), 5);
CHECK_EQ(j.at("noarch"), "generic");
CHECK_EQ(j.at("channel"), "conda-forge");
CHECK_EQ(j.at("url"), "https://repo.mamba.pm/conda-forge/linux-64/foo-4.0-mybld.conda");
CHECK_EQ(j.at("subdir"), "linux-64");
CHECK_EQ(j.at("fn"), "foo-4.0-mybld.conda");
CHECK_EQ(j.at("license"), "MIT");
CHECK_EQ(j.at("size"), 3200);
CHECK_EQ(j.at("timestamp"), 4532);
CHECK_EQ(j.at("sha256"), "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b");
CHECK_EQ(j.at("md5"), "68b329da9893e34099c7d8ad5cb9c940");
CHECK_EQ(j.at("track_features"), "mkl,blas");
CHECK_EQ(j.at("depends"), StrVec{ "python>=3.7", "requests" });
CHECK_EQ(j.at("constrains"), StrVec{ "pip>=2.1" });
}

SUBCASE("from_json")
{
auto j = nl::json::object();
j["name"] = "foo";
j["version"] = "4.0";
j["build_string"] = "mybld";
j["build_number"] = 5;
j["noarch"] = "generic";
j["channel"] = "conda-forge";
j["url"] = "https://repo.mamba.pm/conda-forge/linux-64/foo-4.0-mybld.conda";
j["subdir"] = "linux-64";
j["fn"] = "foo-4.0-mybld.conda";
j["license"] = "MIT";
j["size"] = 3200;
j["timestamp"] = 4532;
j["sha256"] = "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b";
j["md5"] = "68b329da9893e34099c7d8ad5cb9c940";
j["track_features"] = "mkl,blas";
j["depends"] = StrVec{ "python>=3.7", "requests" };
j["constrains"] = StrVec{ "pip>=2.1" };

CHECK_EQ(j.get<PackageInfo>(), pkg);

SUBCASE("noarch")
{
j["noarch"] = "Python";
CHECK_EQ(j.get<PackageInfo>().noarch, NoArchType::Python);
j["noarch"] = true;
CHECK_EQ(j.get<PackageInfo>().noarch, NoArchType::Generic);
j["noarch"] = false;
CHECK_EQ(j.get<PackageInfo>().noarch, NoArchType::No);
j["noarch"] = nullptr;
CHECK_EQ(j.get<PackageInfo>().noarch, NoArchType::No);
j.erase("noarch");
CHECK_EQ(j.get<PackageInfo>().noarch, NoArchType::No);
}

SUBCASE("track_features")
{
j["track_features"] = "python";
CHECK_EQ(j.get<PackageInfo>().track_features, StrVec{ "python" });
j["track_features"] = "python,mkl";
CHECK_EQ(j.get<PackageInfo>().track_features, StrVec{ "python", "mkl" });
j.erase("track_features");
CHECK_EQ(j.get<PackageInfo>().track_features, StrVec{});
j["track_features"] = nl::json::array({ "py", "malloc" });
CHECK_EQ(j.get<PackageInfo>().track_features, StrVec{ "py", "malloc" });
}
}
}
}
1 change: 1 addition & 0 deletions libmambapy/src/libmambapy/bindings/specs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ namespace mambapy
.def("__deepcopy__", &deepcopy<VersionSpec>, py::arg("memo"));

py::class_<PackageInfo>(m, "PackageInfo")
.def_static("from_url", PackageInfo::from_url)
.def(
py::init<std::string, std::string, std::string, std::size_t>(),
py::arg("name") = "",
Expand Down
7 changes: 7 additions & 0 deletions libmambapy/tests/test_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,13 @@ def test_PackageInfo():
# str
assert str(pkg) == "pkg-1.0-bld"

# from_url
pkg = PackageInfo.from_url("https://repo.mamba.pm/conda-forge/linux-64/bar-5.1-xld.conda#01234")
assert pkg.name == "bar"
assert pkg.version == "5.1"
assert pkg.build_string == "xld"
assert pkg.md5 == "01234"

# getters and setters
pkg.name = "foo"
assert pkg.name == "foo"
Expand Down

0 comments on commit 3284572

Please sign in to comment.