diff --git a/libmamba/include/mamba/api/install.hpp b/libmamba/include/mamba/api/install.hpp index aaa9a1441c..60ae51687d 100644 --- a/libmamba/include/mamba/api/install.hpp +++ b/libmamba/include/mamba/api/install.hpp @@ -14,6 +14,7 @@ #include #include +#include "mamba/core/history.hpp" #include "mamba/fs/filesystem.hpp" #include "mamba/solver/request.hpp" diff --git a/libmamba/include/mamba/core/history.hpp b/libmamba/include/mamba/core/history.hpp index cb1e707b90..b8786593f9 100644 --- a/libmamba/include/mamba/core/history.hpp +++ b/libmamba/include/mamba/core/history.hpp @@ -7,7 +7,6 @@ #ifndef MAMBA_CORE_HISTORY #define MAMBA_CORE_HISTORY -#include #include #include #include @@ -15,6 +14,7 @@ #include "mamba/core/channel_context.hpp" #include "mamba/fs/filesystem.hpp" #include "mamba/specs/match_spec.hpp" +#include "mamba/specs/package_info.hpp" namespace mamba { @@ -29,7 +29,7 @@ namespace mamba struct ParseResult { std::string head_line; - std::set diff; + std::vector diff; std::vector comments; }; @@ -38,7 +38,7 @@ namespace mamba static UserRequest prefilled(const Context& context); std::string date; - int revision_num = 0; + std::size_t revision_num = 0; std::string cmd; std::string conda_version; @@ -61,6 +61,23 @@ namespace mamba ChannelContext& m_channel_context; }; + namespace detail + { + /** PackageDiff contains two maps of packages and their package info, one being for the + * installed packages, the other for the removed ones. This is used while looping on + * revisions to get the diff between the target revision and the current one. + */ + struct PackageDiff + { + std::unordered_map removed_pkg_diff; + std::unordered_map installed_pkg_diff; + + [[nodiscard]] static PackageDiff + from_revision(std::vector user_requests, std::size_t target_revision); + }; + + specs::PackageInfo pkg_info_builder(std::string s); + } } // namespace mamba #endif diff --git a/libmamba/src/core/history.cpp b/libmamba/src/core/history.cpp index cf9d6d8ee6..4d60fef2d8 100644 --- a/libmamba/src/core/history.cpp +++ b/libmamba/src/core/history.cpp @@ -4,6 +4,7 @@ // // The full license is in the file LICENSE, distributed with this software. +#include #include #include "mamba/core/channel_context.hpp" @@ -81,12 +82,12 @@ namespace mamba { if (res.size() > 0) { - res[res.size() - 1].diff.insert(line); + res[res.size() - 1].diff.push_back(line); } else { res.push_back(ParseResult()); - res[0].diff.insert(line); + res[0].diff.push_back(line); } } } @@ -203,7 +204,7 @@ namespace mamba std::vector History::get_user_requests() { std::vector res; - int revision_num = 0; + std::size_t revision_num = 0; for (const auto& el : parse()) { UserRequest r; @@ -325,4 +326,207 @@ namespace mamba out << specs_output("neutered", entry.neutered); } } + + namespace detail + { + // The following function parses the different formats that can be found in the history + // file. + // + // conda/mamba1 format: + // + // installed: +conda-forge/linux-64::xtl-0.8.0-h84d6215_0 + // removed: -conda-forge/linux-64::xtl-0.8.0-h84d6215_0 + // + // mamba2 broken format: + // + // installed: +conda-forge::xtl-0.8.0-h84d6215_0 + // removed: -https://conda.anaconda.org/conda-forge/linux-64::xtl-0.8.0-h84d6215_0 + // + // mamba2 new format: + // installed: +https://conda.anaconda.org/conda-forge/linux-64::xtl-0.8.0-h84d6215_0 + // removed: -https://conda.anaconda.org/conda-forge/linux-64::xtl-0.8.0-h84d6215_0 + specs::PackageInfo pkg_info_builder(std::string s) + { + std::string name_version_build_string; + std::string channel; + std::size_t pos_0 = s.rfind("/"); + if (pos_0 != std::string::npos) + { + // s is of the form of + // `https://conda.anaconda.org/conda-forge/linux-64::xtl-0.8.0-h84d6215_0` or + // `conda-forge/linux-64::xtl-0.8.0-h84d6215_0` + std::string s_begin = s.substr(0, pos_0); + // s_begin is of the form of `https://conda.anaconda.org/conda-forge` or + // `conda-forge` + std::string s_end = s.substr(pos_0 + 1, s.size()); + // s_end is of the form of `linux-64::xtl-0.8.0-h84d6215_0` + std::size_t pos = s_begin.rfind("/"); + if (pos != std::string::npos) + { + channel = s_begin.substr(pos + 1, s_begin.size()); + } + else + { + channel = s_begin; + } + name_version_build_string = std::get<1>(util::rsplit_once(s_end, "::")); + } + else + { + // s is of the form of `conda-forge::xtl-0.8.0-h84d6215_0` + auto double_colon_split = util::split_once(s, "::"); + channel = std::get<0>(double_colon_split); + name_version_build_string = std::get<1>(double_colon_split).value_or(""); + } + + // `name_version_build_string` is of the form `xtl-0.8.0-h84d6215_0` + std::size_t pos_1 = name_version_build_string.rfind("-"); + std::string name_version = name_version_build_string.substr(0, pos_1); + // `name_version` is of the form `xtl-0.8.0` + std::string build_string = name_version_build_string.substr( + pos_1 + 1, + name_version_build_string.size() + ); + + std::size_t pos_2 = name_version.rfind("-"); + std::string name = name_version.substr(0, pos_2); + std::string version = name_version.substr(pos_2 + 1, name_version.size()); + + specs::PackageInfo pkg_info{ name, version, build_string, channel }; + return pkg_info; + } + + PackageDiff PackageDiff::from_revision( + std::vector user_requests, + std::size_t target_revision + ) + { + assert(!user_requests.empty()); + + struct revision + { + std::size_t key = 0; + std::unordered_map removed_pkg = {}; + std::unordered_map installed_pkg = {}; + }; + + std::list revisions; + for (auto r : user_requests) + { + if ((r.link_dists.size() > 0) || (r.unlink_dists.size() > 0)) + { + if (r.revision_num > target_revision) + { + revision rev{ /*.key = */ r.revision_num }; + for (auto ud : r.unlink_dists) + { + auto pkg_info = pkg_info_builder(ud); + const auto name = pkg_info.name; + rev.removed_pkg[name] = std::move(pkg_info); + } + for (auto ld : r.link_dists) + { + auto pkg_info = pkg_info_builder(ld); + const auto name = pkg_info.name; + rev.installed_pkg[name] = std::move(pkg_info); + } + revisions.push_back(rev); + } + } + } + + PackageDiff pkg_diff{}; + + const auto handle_install = [&pkg_diff](revision& rev, const std::string& pkg_name) + { + bool res = false; + if (auto rev_iter = rev.installed_pkg.find(pkg_name); + rev_iter != rev.installed_pkg.end()) + { + const auto version = rev_iter->second.version; + auto iter = pkg_diff.removed_pkg_diff.find(pkg_name); + if (iter != pkg_diff.removed_pkg_diff.end() && iter->second.version == version) + { + pkg_diff.removed_pkg_diff.erase(iter); + } + else + { + pkg_diff.installed_pkg_diff[pkg_name] = rev_iter->second; + } + rev.installed_pkg.erase(rev_iter); + res = true; + } + return res; + }; + + auto handle_remove = [&pkg_diff](revision& rev, const std::string& pkg_name) + { + bool res = false; + if (auto rev_iter = rev.removed_pkg.find(pkg_name); rev_iter != rev.removed_pkg.end()) + { + const auto version = rev_iter->second.version; + auto iter = pkg_diff.installed_pkg_diff.find(pkg_name); + if (iter != pkg_diff.installed_pkg_diff.end() && iter->second.version == version) + { + pkg_diff.installed_pkg_diff.erase(iter); + } + else + { + pkg_diff.removed_pkg_diff[pkg_name] = rev_iter->second; + } + rev.removed_pkg.erase(rev_iter); + res = true; + } + return res; + }; + + while (!revisions.empty()) + { + auto& revision = *(revisions.begin()); + while (!revision.removed_pkg.empty()) + { + auto [pkg_name, pkg_info] = *(revision.removed_pkg.begin()); + pkg_diff.removed_pkg_diff[pkg_name] = pkg_info; + revision.removed_pkg.erase(pkg_name); + bool lastly_removed = true; // whether last operation on package was a removal + lastly_removed = !handle_install(revision, pkg_name); + for (auto rev = std::next(revisions.begin()); rev != revisions.end(); ++rev) + { + if (lastly_removed) + { + lastly_removed = !handle_install(*rev, pkg_name); + } + else + { + lastly_removed = handle_remove(*rev, pkg_name); + if (lastly_removed) + { + lastly_removed = !handle_install(*rev, pkg_name); + } + } + } + } + while (!revision.installed_pkg.empty()) + { + auto [pkg_name, pkg_info] = *(revision.installed_pkg.begin()); + pkg_diff.installed_pkg_diff[pkg_name] = pkg_info; + revision.installed_pkg.erase(pkg_name); + bool lastly_removed = false; + for (auto rev = ++revisions.begin(); rev != revisions.end(); ++rev) + { + if (!lastly_removed) + { + lastly_removed = handle_remove(*rev, pkg_name); + if (lastly_removed) + { + lastly_removed = !handle_install(*rev, pkg_name); + } + } + } + } + revisions.pop_front(); + } + return pkg_diff; + } + } // namespace detail } // namespace mamba diff --git a/libmamba/tests/data/history/parse/conda-meta/aux_file b/libmamba/tests/data/history/parse/conda-meta/aux_file index 8026b26853..a08f5c010f 100644 --- a/libmamba/tests/data/history/parse/conda-meta/aux_file +++ b/libmamba/tests/data/history/parse/conda-meta/aux_file @@ -1,24 +1,74 @@ -==> 2020-05-03 11:18:43 <== -# cmd: /usr/bin/conda create -n mamba -# conda version: 4.8.3 -==> 2020-05-03 11:19:42 <== -# cmd: /usr/bin/conda install pybind11 libsolv libarchive libcurl nlohmann_json pip cpp-tabulate -c conda-forge -# conda version: 4.8.3 -+conda-forge/linux-64::_libgcc_mutex-0.1-conda_forge -# update specs: ["cpp-tabulate", "libsolv", "nlohmann_json", "pybind11", "pip", "libarchive", "libcurl"] -==> 2020-05-04 08:58:01 <== -# cmd: /home/mariana/.local/bin/mamba remove cpp-tabulate pybind11 pip -# conda version: 4.8.3 --conda-forge/noarch::wheel-0.34.2-py_1 -# remove specs: ["cpp-tabulate", "pybind11", "pip"] -==> 2020-05-04 08:59:15 <== -# cmd: /home/mariana/.local/bin/mamba install -c conda-forge cpp-tabulate==1.0.0 -# conda version: 4.8.3 -+conda-forge/linux-64::cpp-tabulate-1.0-hc9558a2_0 -# update specs: ["cpp-tabulate==1.0.0"] -==> 2020-05-04 09:00:02 <== -# cmd: /home/mariana/.local/bin/mamba upgrade -c conda-forge cpp-tabulate -# conda version: 4.8.3 --conda-forge/linux-64::cpp-tabulate-1.0-hc9558a2_0 -+conda-forge/linux-64::cpp-tabulate-1.2-hc9558a2_0 +==> 2025-04-14 09:23:10 <== +# cmd: /home/sandrinepataut/.local/bin/micromamba install nlohmann_json +# conda version: 3.8.0 ++conda-forge::nlohmann_json-3.12.0-h3f2d84a_0 +# update specs: ["nlohmann_json"] +==> 2025-04-14 09:23:50 <== +# cmd: /home/sandrinepataut/.local/bin/micromamba install xtl=0.7.2 +# conda version: 3.8.0 ++conda-forge::libgomp-14.2.0-h767d61c_2 ++conda-forge::_libgcc_mutex-0.1-conda_forge ++conda-forge::_openmp_mutex-4.5-2_gnu ++conda-forge::libgcc-14.2.0-h767d61c_2 ++conda-forge::libstdcxx-14.2.0-h8f9b012_2 ++conda-forge::libgcc-ng-14.2.0-h69a702a_2 ++conda-forge::libstdcxx-ng-14.2.0-h4852527_2 ++conda-forge::xtl-0.7.2-h4bd325d_1 +# update specs: ["xtl=0.7.2"] +==> 2025-04-14 09:24:22 <== +# cmd: /home/sandrinepataut/.local/bin/micromamba install cpp-tabulate=1.0 +# conda version: 3.8.0 ++conda-forge::cpp-tabulate-1.0-hc9558a2_0 +# update specs: ["cpp-tabulate=1.0"] +==> 2025-04-14 09:24:49 <== +# cmd: /home/sandrinepataut/.local/bin/micromamba update cpp-tabulate +# conda version: 3.8.0 +-https://conda.anaconda.org/conda-forge/linux-64::cpp-tabulate-1.0-hc9558a2_0 ++conda-forge::cpp-tabulate-1.5-hf52228f_1 # update specs: ["cpp-tabulate"] +# remove specs: ["cpp-tabulate"] +==> 2025-04-14 09:25:49 <== +# cmd: /home/sandrinepataut/.local/bin/micromamba install wheel=0.34.2 openssl=3.5.0 +# conda version: 3.8.0 ++conda-forge::libexpat-2.7.0-h5888daf_0 ++conda-forge::liblzma-5.8.1-hb9d3cd8_0 ++conda-forge::libmpdec-4.0.0-h4bc722e_0 ++conda-forge::libuuid-2.38.1-h0b41bf4_0 ++conda-forge::bzip2-1.0.8-h4bc722e_7 ++conda-forge::ld_impl_linux-64-2.43-h712a8e2_4 ++conda-forge::libffi-3.4.6-h2dba641_1 ++conda-forge::libzlib-1.3.1-hb9d3cd8_2 ++conda-forge::ncurses-6.5-h2d0b736_3 ++conda-forge::python_abi-3.13-6_cp313 ++conda-forge::ca-certificates-2025.1.31-hbcca054_0 ++conda-forge::tk-8.6.13-noxft_h4845f30_101 ++conda-forge::libsqlite-3.49.1-hee588c1_2 ++conda-forge::readline-8.2-h8c095d6_2 ++conda-forge::openssl-3.5.0-h7b32b05_0 ++conda-forge::tzdata-2025b-h78e105d_0 ++conda-forge::python-3.13.3-hf636f53_100_cp313 ++conda-forge::pip-25.0.1-pyh145f28c_0 ++conda-forge::setuptools-78.1.0-pyhff2d567_0 ++conda-forge::wheel-0.34.2-py_1 +# update specs: ["wheel=0.34.2", "openssl=3.5.0"] +==> 2025-04-14 09:26:26 <== +# cmd: /home/sandrinepataut/.local/bin/micromamba update wheel +# conda version: 3.8.0 +-https://conda.anaconda.org/conda-forge/noarch::setuptools-78.1.0-pyhff2d567_0 +-https://conda.anaconda.org/conda-forge/noarch::wheel-0.34.2-py_1 ++conda-forge::wheel-0.45.1-pyhd8ed1ab_1 +# update specs: ["wheel"] +# remove specs: ["wheel"] +==> 2025-04-14 09:27:06 <== +# cmd: /home/sandrinepataut/.local/bin/micromamba remove wheel xtl nlohmann_json +# conda version: 3.8.0 +-https://conda.anaconda.org/conda-forge/linux-64::nlohmann_json-3.12.0-h3f2d84a_0 +-https://conda.anaconda.org/conda-forge/noarch::wheel-0.45.1-pyhd8ed1ab_1 +-https://conda.anaconda.org/conda-forge/linux-64::xtl-0.7.2-h4bd325d_1 +# remove specs: ["wheel", "xtl", "nlohmann_json"] +==> 2025-04-14 09:27:41 <== +# cmd: /home/sandrinepataut/.local/bin/micromamba install wheel=0.40.0 xtl=0.8.0 +# conda version: 3.8.0 ++conda-forge::xtl-0.8.0-h84d6215_0 ++conda-forge::wheel-0.40.0-pyhd8ed1ab_1 +# update specs: ["wheel=0.40.0", "xtl=0.8.0"] diff --git a/libmamba/tests/data/history/parse/conda-meta/history b/libmamba/tests/data/history/parse/conda-meta/history index 8026b26853..a08f5c010f 100644 --- a/libmamba/tests/data/history/parse/conda-meta/history +++ b/libmamba/tests/data/history/parse/conda-meta/history @@ -1,24 +1,74 @@ -==> 2020-05-03 11:18:43 <== -# cmd: /usr/bin/conda create -n mamba -# conda version: 4.8.3 -==> 2020-05-03 11:19:42 <== -# cmd: /usr/bin/conda install pybind11 libsolv libarchive libcurl nlohmann_json pip cpp-tabulate -c conda-forge -# conda version: 4.8.3 -+conda-forge/linux-64::_libgcc_mutex-0.1-conda_forge -# update specs: ["cpp-tabulate", "libsolv", "nlohmann_json", "pybind11", "pip", "libarchive", "libcurl"] -==> 2020-05-04 08:58:01 <== -# cmd: /home/mariana/.local/bin/mamba remove cpp-tabulate pybind11 pip -# conda version: 4.8.3 --conda-forge/noarch::wheel-0.34.2-py_1 -# remove specs: ["cpp-tabulate", "pybind11", "pip"] -==> 2020-05-04 08:59:15 <== -# cmd: /home/mariana/.local/bin/mamba install -c conda-forge cpp-tabulate==1.0.0 -# conda version: 4.8.3 -+conda-forge/linux-64::cpp-tabulate-1.0-hc9558a2_0 -# update specs: ["cpp-tabulate==1.0.0"] -==> 2020-05-04 09:00:02 <== -# cmd: /home/mariana/.local/bin/mamba upgrade -c conda-forge cpp-tabulate -# conda version: 4.8.3 --conda-forge/linux-64::cpp-tabulate-1.0-hc9558a2_0 -+conda-forge/linux-64::cpp-tabulate-1.2-hc9558a2_0 +==> 2025-04-14 09:23:10 <== +# cmd: /home/sandrinepataut/.local/bin/micromamba install nlohmann_json +# conda version: 3.8.0 ++conda-forge::nlohmann_json-3.12.0-h3f2d84a_0 +# update specs: ["nlohmann_json"] +==> 2025-04-14 09:23:50 <== +# cmd: /home/sandrinepataut/.local/bin/micromamba install xtl=0.7.2 +# conda version: 3.8.0 ++conda-forge::libgomp-14.2.0-h767d61c_2 ++conda-forge::_libgcc_mutex-0.1-conda_forge ++conda-forge::_openmp_mutex-4.5-2_gnu ++conda-forge::libgcc-14.2.0-h767d61c_2 ++conda-forge::libstdcxx-14.2.0-h8f9b012_2 ++conda-forge::libgcc-ng-14.2.0-h69a702a_2 ++conda-forge::libstdcxx-ng-14.2.0-h4852527_2 ++conda-forge::xtl-0.7.2-h4bd325d_1 +# update specs: ["xtl=0.7.2"] +==> 2025-04-14 09:24:22 <== +# cmd: /home/sandrinepataut/.local/bin/micromamba install cpp-tabulate=1.0 +# conda version: 3.8.0 ++conda-forge::cpp-tabulate-1.0-hc9558a2_0 +# update specs: ["cpp-tabulate=1.0"] +==> 2025-04-14 09:24:49 <== +# cmd: /home/sandrinepataut/.local/bin/micromamba update cpp-tabulate +# conda version: 3.8.0 +-https://conda.anaconda.org/conda-forge/linux-64::cpp-tabulate-1.0-hc9558a2_0 ++conda-forge::cpp-tabulate-1.5-hf52228f_1 # update specs: ["cpp-tabulate"] +# remove specs: ["cpp-tabulate"] +==> 2025-04-14 09:25:49 <== +# cmd: /home/sandrinepataut/.local/bin/micromamba install wheel=0.34.2 openssl=3.5.0 +# conda version: 3.8.0 ++conda-forge::libexpat-2.7.0-h5888daf_0 ++conda-forge::liblzma-5.8.1-hb9d3cd8_0 ++conda-forge::libmpdec-4.0.0-h4bc722e_0 ++conda-forge::libuuid-2.38.1-h0b41bf4_0 ++conda-forge::bzip2-1.0.8-h4bc722e_7 ++conda-forge::ld_impl_linux-64-2.43-h712a8e2_4 ++conda-forge::libffi-3.4.6-h2dba641_1 ++conda-forge::libzlib-1.3.1-hb9d3cd8_2 ++conda-forge::ncurses-6.5-h2d0b736_3 ++conda-forge::python_abi-3.13-6_cp313 ++conda-forge::ca-certificates-2025.1.31-hbcca054_0 ++conda-forge::tk-8.6.13-noxft_h4845f30_101 ++conda-forge::libsqlite-3.49.1-hee588c1_2 ++conda-forge::readline-8.2-h8c095d6_2 ++conda-forge::openssl-3.5.0-h7b32b05_0 ++conda-forge::tzdata-2025b-h78e105d_0 ++conda-forge::python-3.13.3-hf636f53_100_cp313 ++conda-forge::pip-25.0.1-pyh145f28c_0 ++conda-forge::setuptools-78.1.0-pyhff2d567_0 ++conda-forge::wheel-0.34.2-py_1 +# update specs: ["wheel=0.34.2", "openssl=3.5.0"] +==> 2025-04-14 09:26:26 <== +# cmd: /home/sandrinepataut/.local/bin/micromamba update wheel +# conda version: 3.8.0 +-https://conda.anaconda.org/conda-forge/noarch::setuptools-78.1.0-pyhff2d567_0 +-https://conda.anaconda.org/conda-forge/noarch::wheel-0.34.2-py_1 ++conda-forge::wheel-0.45.1-pyhd8ed1ab_1 +# update specs: ["wheel"] +# remove specs: ["wheel"] +==> 2025-04-14 09:27:06 <== +# cmd: /home/sandrinepataut/.local/bin/micromamba remove wheel xtl nlohmann_json +# conda version: 3.8.0 +-https://conda.anaconda.org/conda-forge/linux-64::nlohmann_json-3.12.0-h3f2d84a_0 +-https://conda.anaconda.org/conda-forge/noarch::wheel-0.45.1-pyhd8ed1ab_1 +-https://conda.anaconda.org/conda-forge/linux-64::xtl-0.7.2-h4bd325d_1 +# remove specs: ["wheel", "xtl", "nlohmann_json"] +==> 2025-04-14 09:27:41 <== +# cmd: /home/sandrinepataut/.local/bin/micromamba install wheel=0.40.0 xtl=0.8.0 +# conda version: 3.8.0 ++conda-forge::xtl-0.8.0-h84d6215_0 ++conda-forge::wheel-0.40.0-pyhd8ed1ab_1 +# update specs: ["wheel=0.40.0", "xtl=0.8.0"] diff --git a/libmamba/tests/src/core/test_history.cpp b/libmamba/tests/src/core/test_history.cpp index 142940bdf3..fb864fdce8 100644 --- a/libmamba/tests/src/core/test_history.cpp +++ b/libmamba/tests/src/core/test_history.cpp @@ -21,6 +21,7 @@ #include "mamba/core/channel_context.hpp" #include "mamba/core/history.hpp" +#include "mamba/core/prefix_data.hpp" #include "mambatests.hpp" @@ -105,6 +106,45 @@ namespace mamba std::vector user_reqs = history_instance.get_user_requests(); } + TEST_CASE("parse_all_formats") + { + std::vector test_list{ + "conda-forge/linux-64::xtl-0.8.0-h84d6215_0", + "conda-forge::xtl-0.8.0-h84d6215_0", + "https://conda.anaconda.org/conda-forge/linux-64::xtl-0.8.0-h84d6215_0" + }; + for (auto s : test_list) + { + specs::PackageInfo pkg_info = mamba::detail::pkg_info_builder(s); + REQUIRE(pkg_info.name == "xtl"); + REQUIRE(pkg_info.version == "0.8.0"); + REQUIRE(pkg_info.channel == "conda-forge"); + REQUIRE(pkg_info.build_string == "h84d6215_0"); + } + } + + TEST_CASE("revision_diff") + { + auto channel_context = ChannelContext::make_conda_compatible(mambatests::context()); + + // Gather history from current history file. + History history_instance(mambatests::test_data_dir / "history/parse", channel_context); + std::vector user_requests = history_instance.get_user_requests(); + std::size_t target_revision = 1; + + detail::PackageDiff pkg_diff{}; + pkg_diff = pkg_diff.from_revision(user_requests, target_revision); + const auto& removed_pkg_diff = pkg_diff.removed_pkg_diff; + const auto& installed_pkg_diff = pkg_diff.installed_pkg_diff; + + REQUIRE(removed_pkg_diff.find("nlohmann_json")->second.version == "3.12.0"); + REQUIRE(removed_pkg_diff.find("xtl")->second.version == "0.7.2"); + REQUIRE(installed_pkg_diff.find("cpp-tabulate")->second.version == "1.5"); + REQUIRE(installed_pkg_diff.find("wheel")->second.version == "0.40.0"); + REQUIRE(installed_pkg_diff.find("openssl")->second.version == "3.5.0"); + REQUIRE(installed_pkg_diff.find("xtl")->second.version == "0.8.0"); + } + #ifndef _WIN32 TEST_CASE("parse_segfault") {