LCOV - code coverage report
Current view: top level - src/database - ParquetManager.test.cpp (source / functions) Hit Total Coverage
Test: coverage.info Lines: 109 123 88.6 %
Date: 2026-10-14 13:16:16 Functions: 8 8 100.0 %

          Line data    Source code
       1             : // SPDX-FileCopyrightText: 2025 PairInteraction Developers
       2             : // SPDX-License-Identifier: LGPL-3.0-or-later
       3             : 
       4             : #include "pairinteraction/database/ParquetManager.hpp"
       5             : 
       6             : #include "pairinteraction/database/Database.hpp"
       7             : #include "pairinteraction/database/GitHubDownloader.hpp"
       8             : 
       9             : #include <doctest/doctest.h>
      10             : #include <duckdb.hpp>
      11             : #include <filesystem>
      12             : #include <fmt/format.h>
      13             : #include <fstream>
      14             : #include <memory>
      15             : #include <miniz.h>
      16             : #include <nlohmann/json.hpp>
      17             : 
      18             : namespace pairinteraction {
      19             : class MockDownloader : public GitHubDownloader {
      20             : public:
      21             :     std::future<GitHubDownloader::Result>
      22          12 :     download(const std::string &remote_url, const std::string & /*if_none_match*/ = "",
      23             :              bool /*use_octet_stream*/ = false) const override {
      24          12 :         GitHubDownloader::Result result;
      25          12 :         result.status_code = 200;
      26          12 :         result.rate_limit.remaining = 60;
      27          12 :         result.rate_limit.reset_time = 2147483647;
      28             : 
      29          12 :         if (remote_url == "/test/repo/path") {
      30             :             // This is a repo path request for a single release, return JSON with assets
      31           2 :             result.body = make_release("2.1").dump();
      32          11 :         } else if (remote_url == "/test/repo/releases") {
      33             :             // This is a repo path request for a list of releases. The latest release only
      34             :             // provides tables whose major version differs from COMPATIBLE_DATABASE_VERSION_MAJOR
      35             :             // and is thus incompatible, the older ones are compatible. In addition, the oldest
      36             :             // release contains an asset that is not provided anymore by newer releases.
      37           2 :             nlohmann::json releases = nlohmann::json::array();
      38           4 :             releases.push_back(make_release("3.0"));
      39           4 :             releases.push_back(make_release("2.1"));
      40           6 :             releases.push_back(make_release("2.0", {"misc", "retired"}));
      41           2 :             result.body = releases.dump();
      42          11 :         } else if (remote_url == "/test/repo/releases_unpublished") {
      43             :             // This is a repo path request for a list of releases whose latest entries are a draft
      44             :             // and a pre-release. They must be skipped although they provide compatible tables. The
      45             :             // draft does not contain any assets at all, as it is the case for drafts on GitHub
      46             :             // that have not been populated yet.
      47           1 :             nlohmann::json draft;
      48           1 :             draft["draft"] = true;
      49             : 
      50           2 :             nlohmann::json prerelease = make_release("2.4");
      51           1 :             prerelease["prerelease"] = true;
      52             : 
      53           1 :             nlohmann::json releases = nlohmann::json::array();
      54           1 :             releases.push_back(draft);
      55           1 :             releases.push_back(prerelease);
      56           2 :             releases.push_back(make_release("2.1"));
      57           1 :             result.body = releases.dump();
      58           9 :         } else if (remote_url == "/test/repo/releases_invalid") {
      59             :             // This is a repo path request whose response is not valid JSON
      60           1 :             result.body = "not a json";
      61           7 :         } else if (remote_url == "/rate_limit") {
      62             :             // This is the rate limit request
      63           4 :             result.body = "";
      64             :         } else {
      65             :             // This is the file download request
      66           3 :             std::string content = "updated_file_content";
      67           3 :             std::string filename = "misc_v2.1/wigner.parquet";
      68             : 
      69           3 :             mz_zip_archive zip_archive{};
      70           3 :             size_t zip_size = 0;
      71           3 :             void *zip_data = nullptr;
      72             : 
      73           3 :             mz_zip_writer_init_heap(&zip_archive, 0, 0);
      74           3 :             mz_zip_writer_add_mem(&zip_archive, filename.c_str(), content.data(), content.size(),
      75             :                                   MZ_BEST_SPEED);
      76           3 :             mz_zip_writer_finalize_heap_archive(&zip_archive, &zip_data, &zip_size);
      77             : 
      78           3 :             result.body = std::string(static_cast<char *>(zip_data), zip_size);
      79             : 
      80           3 :             mz_free(zip_data);
      81           3 :             mz_zip_writer_end(&zip_archive);
      82           3 :         }
      83             : 
      84          48 :         return std::async(std::launch::deferred, [result]() { return result; });
      85          37 :     }
      86             : 
      87             : private:
      88             :     // Construct a release that provides the tables of the given version for the given assets
      89           9 :     static nlohmann::json make_release(const std::string &version,
      90             :                                        const std::vector<std::string> &keys = {"misc"}) {
      91           9 :         nlohmann::json assets = nlohmann::json::array();
      92          20 :         for (const auto &key : keys) {
      93          11 :             nlohmann::json asset;
      94          22 :             asset["name"] = fmt::format("{}_v{}.zip", key, version);
      95          22 :             asset["url"] = fmt::format("https://api.github.com/test/path/{}_v{}.zip", key, version);
      96          11 :             assets.push_back(asset);
      97          11 :         }
      98             : 
      99           9 :         nlohmann::json release;
     100           9 :         release["assets"] = assets;
     101          18 :         return release;
     102           9 :     }
     103             : };
     104             : 
     105           6 : TEST_CASE("ParquetManager functionality with mocked downloader") {
     106           6 :     MockDownloader downloader;
     107           6 :     auto test_dir = std::filesystem::temp_directory_path() / "pairinteraction_test_db";
     108           6 :     duckdb::DuckDB db(nullptr);
     109           6 :     duckdb::Connection con(db);
     110             : 
     111             :     // Create a manager for the given repository paths and scan the local and remote tables
     112           6 :     auto make_manager = [&](std::vector<std::string> repo_paths) {
     113          12 :         auto manager = std::make_unique<ParquetManager>(test_dir, downloader, std::move(repo_paths),
     114           6 :                                                         con, false);
     115           6 :         manager->scan_local();
     116           6 :         manager->scan_remote();
     117           6 :         return manager;
     118           0 :     };
     119             : 
     120             :     // Construct the path to the wigner table of the misc asset of the given version
     121          19 :     auto wigner_path = [&](const std::string &version) {
     122          57 :         return (test_dir / "tables" / fmt::format("misc_v{}", version) / "wigner.parquet").string();
     123           6 :     };
     124             : 
     125             :     // Create a local wigner table of the misc asset of the given version
     126          13 :     auto add_local_table = [&](const std::string &version) {
     127          26 :         std::filesystem::create_directories(test_dir / "tables" / fmt::format("misc_v{}", version));
     128          13 :         std::ofstream(wigner_path(version)).close();
     129          13 :     };
     130             : 
     131             :     // Provide a compatible version and an incompatible version with a higher minor version
     132           6 :     add_local_table("2.0");
     133           6 :     add_local_table("1.4");
     134             : 
     135           6 :     SUBCASE("Check missing table") {
     136           1 :         auto manager = make_manager({});
     137             : 
     138           5 :         CHECK_THROWS_WITH_AS(manager->get_path("misc", "missing_table"),
     139             :                              "No table 'missing_table.parquet' found for species 'misc'. The "
     140             :                              "tables for the species are incomplete.",
     141             :                              std::runtime_error);
     142           7 :     }
     143             : 
     144           6 :     SUBCASE("Check version parsing") {
     145             :         // Provide a second compatible version in addition
     146           1 :         add_local_table("2.2");
     147             : 
     148           1 :         auto manager = make_manager({});
     149             : 
     150             :         // The newest compatible version must be used, neither the oldest compatible version nor
     151             :         // the incompatible version with the highest minor version
     152           1 :         CHECK(manager->get_path("misc", "wigner") == wigner_path("2.2"));
     153           7 :     }
     154             : 
     155           6 :     SUBCASE("Check update table") {
     156           2 :         auto manager = make_manager({"/test/repo/path"});
     157             : 
     158           1 :         CHECK(manager->get_path("misc", "wigner") == wigner_path("2.1"));
     159             : 
     160           1 :         std::ifstream in(wigner_path("2.1"), std::ios::binary);
     161           1 :         std::stringstream buffer;
     162           1 :         buffer << in.rdbuf();
     163           1 :         CHECK(buffer.str() == "updated_file_content");
     164           7 :     }
     165             : 
     166           6 :     SUBCASE("Check update table if the latest release is incompatible") {
     167           2 :         auto manager = make_manager({"/test/repo/releases"});
     168             : 
     169             :         // The latest compatible release must be used, not the latest release
     170           1 :         CHECK(manager->get_path("misc", "wigner") == wigner_path("2.1"));
     171             : 
     172             :         // Assets that are only provided by older releases must not be used
     173           5 :         CHECK_THROWS_WITH_AS(
     174             :             manager->get_path("retired", "wigner"),
     175             :             "No tables found for species 'retired'. Check the spelling of the species.",
     176             :             std::runtime_error);
     177           7 :     }
     178             : 
     179           6 :     SUBCASE("Check invalid response of a repository") {
     180             :         // If one repository returns an invalid response, the download of database tables must be
     181             :         // disabled altogether so that the local tables are used instead of silently providing an
     182             :         // incomplete set of tables
     183           3 :         auto manager = make_manager({"/test/repo/releases", "/test/repo/releases_invalid"});
     184             : 
     185           1 :         CHECK(manager->get_path("misc", "wigner") == wigner_path("2.0"));
     186           7 :     }
     187             : 
     188           6 :     SUBCASE("Check update table if the latest releases are unpublished") {
     189           2 :         auto manager = make_manager({"/test/repo/releases_unpublished"});
     190             : 
     191             :         // The latest published release must be used, neither the draft nor the pre-release
     192           1 :         CHECK(manager->get_path("misc", "wigner") == wigner_path("2.1"));
     193           7 :     }
     194             : 
     195           6 :     std::filesystem::remove_all(test_dir);
     196          14 : }
     197             : 
     198           1 : DOCTEST_TEST_CASE("ParquetManager functionality with GitHub downloader") {
     199           1 :     if (!Database::get_global_instance().get_download_missing()) {
     200           1 :         DOCTEST_MESSAGE("Skipping test because download_missing is false.");
     201           1 :         return;
     202             :     }
     203           0 :     GitHubDownloader downloader;
     204           0 :     duckdb::DuckDB db(nullptr);
     205           0 :     duckdb::Connection con(db);
     206             : 
     207             :     std::vector<std::string> repo_paths = {
     208             :         "/repos/pairinteraction/database-sqdt/releases?per_page=100",
     209           0 :         "/repos/pairinteraction/database-mqdt/releases?per_page=100"};
     210           0 :     ParquetManager manager(Database::get_global_instance().get_database_dir(), downloader,
     211           0 :                            repo_paths, con, Database::get_global_instance().get_use_cache());
     212           0 :     manager.scan_local();
     213           0 :     manager.scan_remote();
     214             : 
     215           0 :     std::string info = manager.get_versions_info();
     216             : 
     217             :     // Check that all species are present
     218             :     std::vector<std::string> should_contain = {
     219             :         "Cs",        "K",         "Li",         "Na",         "Rb",         "Sr87_mqdt",
     220           0 :         "Sr88_sqdt", "Sr88_mqdt", "Yb171_mqdt", "Yb173_mqdt", "Yb174_mqdt", "misc"};
     221           0 :     for (const auto &substr : should_contain) {
     222           0 :         DOCTEST_CHECK(info.find(substr) != std::string::npos);
     223             :     }
     224           0 : }
     225             : 
     226             : } // namespace pairinteraction

Generated by: LCOV version 1.16