You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iggy.apache.org by "slbotbm (via GitHub)" <gi...@apache.org> on 2026/04/12 14:28:56 UTC

[PR] feata(cpp): add functions related to system, tests for purge_* functions, and consumer group functions (WIP) [iggy]

slbotbm opened a new pull request, #3102:
URL: https://github.com/apache/iggy/pull/3102

   ## Which issue does this PR close?
   
   <!--
   We generally require a GitHub issue to be filed for all bug fixes and enhancements. You can link an issue to this PR using the GitHub syntax. For example `Closes #123` indicates that this PR will close issue #123.
   -->
   
   Works toward completion of #2100 
   
   ## Rationale
   
   This PR adds functions related to the system, and tests for previously untested functions.
   
   ## What changed?
   
   This PR adds:
   - The following functions and their tests:
       - get_stats
       - get_me
       - get_client
       - get_clients
       - ping
       - heartbeat_interval
       - snapshot
       - purge_stream (TODO)
       - purge_topic (TODO)
   - tests for join_consumer_group and leave_consumer_group (TODO)
   
   The places labelled TODO will be completed after merging of #3046.
   ## Local Execution
   
   - Passed 
   - Pre-commit hooks ran
   
   <!--
   You must run your code locally before submitting.
   "Relying on CI" is not acceptable - PRs from authors who haven't run the code will be closed.
   
   Did you have `prek` installed? It runs automatically on commit and covers all project languages. See [CONTRIBUTING.md](https://github.com/apache/iggy/blob/master/CONTRIBUTING.md).
   -->
   
   ## AI Usage
   
   
   If AI tools were used, please answer:
   1. Which tools? (e.g., GitHub Copilot, Claude, ChatGPT) codex
   2. Scope of usage? (e.g., autocomplete, generated functions, entire implementation) code generation based on explicit instructions given by me.
   3. How did you verify the generated code works correctly? ran the tests, read through and understood the generated code
   4. Can you explain every line of the code if asked? yes
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "hubcio (via GitHub)" <gi...@apache.org>.
hubcio merged PR #3102:
URL: https://github.com/apache/iggy/pull/3102


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "amlel-el-mahrouss (via GitHub)" <gi...@apache.org>.
amlel-el-mahrouss commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3282511779


##########
foreign/cpp/include/iggy.hpp:
##########
@@ -26,46 +26,128 @@
 
 namespace iggy {
 
+/// Exception raised by the C++ client when an operation fails.
 class IggyException : public std::runtime_error {
   public:
     explicit IggyException(const char *message) : std::runtime_error(message) {}
     explicit IggyException(const std::string &message) : std::runtime_error(message) {}
 };
 
-class CompressionAlgorithm final {
+namespace detail {
+
+/// Internal helper used by string-backed option types in this header.
+template <typename Tag>
+class StringTag {
+  protected:
+    explicit StringTag(std::string value) : value_(std::move(value)) {}
+
+    std::string_view value() const { return value_; }
+
+  private:
+    std::string value_;
+};
+
+}  // namespace detail
+
+/// Compression setting for `Client::create_topic(...)`.
+///
+/// Use this type to choose whether messages in a topic are stored as-is or compressed with gzip.
+///
+/// Internally, this value is passed across the Rust FFI as a string.
+/// Values outside the supported set are rejected by the Rust client.
+/// Some Rust-side aliases may also be accepted for compatibility.
+class CompressionAlgorithm final : private detail::StringTag<CompressionAlgorithm> {
   public:
+    /// Store messages without compression.
     static CompressionAlgorithm none() { return CompressionAlgorithm("none"); }
+    /// Compress messages with gzip.
     static CompressionAlgorithm gzip() { return CompressionAlgorithm("gzip"); }
 
-    std::string_view compression_algorithm_value() const { return algorithm_; }
+    std::string_view compression_algorithm_value() const { return value(); }
 
   private:
-    explicit CompressionAlgorithm(std::string algorithm) : algorithm_(std::move(algorithm)) {}
-
-    std::string algorithm_;
+    explicit CompressionAlgorithm(std::string algorithm)
+        : detail::StringTag<CompressionAlgorithm>(std::move(algorithm)) {}
 };
 
-class Expiry final {
+/// Compression setting for `Client::snapshot(...)`.
+///
+/// Use this type to choose how snapshot data is compressed in the generated archive.
+///
+/// Internally, this value is passed across the Rust FFI as a string.
+/// Values outside the supported set are rejected by the Rust client.
+/// Some Rust-side aliases may also be accepted for compatibility.
+class SnapshotCompression final : private detail::StringTag<SnapshotCompression> {
   public:
-    static Expiry server_default() { return Expiry("server_default", 0); }
-    static Expiry never_expire() { return Expiry("never_expire", std::numeric_limits<std::uint64_t>::max()); }
-    static Expiry duration(std::uint64_t micros) { return Expiry("duration", micros); }
-
-    std::string_view expiry_kind() const { return expiry_kind_; }
-    std::uint64_t expiry_value() const { return expiry_value_; }
+    /// Store snapshot files without compression.
+    static SnapshotCompression stored() { return SnapshotCompression("stored"); }
+    /// Use standard deflate compression.
+    static SnapshotCompression deflated() { return SnapshotCompression("deflated"); }
+    /// Use bzip2 for a higher compression ratio at the cost of slower processing.
+    static SnapshotCompression bzip2() { return SnapshotCompression("bzip2"); }
+    /// Use Zstandard for fast compression and decompression.
+    static SnapshotCompression zstd() { return SnapshotCompression("zstd"); }
+    /// Use LZMA for high compression, especially for larger files.
+    static SnapshotCompression lzma() { return SnapshotCompression("lzma"); }
+    /// Use XZ, which is similar to LZMA and often faster to decompress.
+    static SnapshotCompression xz() { return SnapshotCompression("xz"); }
+
+    std::string_view snapshot_compression_value() const { return value(); }

Review Comment:
   Shouldn't this be in a trait type instead?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3252442164


##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -149,3 +153,671 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));

Review Comment:
   Will be addressed in the next PR. I will create proper clean up for the tests.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3252492489


##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -149,3 +153,671 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add a test to create some streams, topics, partitions, and segments, send messages, and create
+// consumer groups and verify it.
+TEST(LowLevelE2E_Client, GetStatsReturnsServerStats) {
+    RecordProperty("description",
+                   "Returns empty resource counts first, then reflects aggregated streams, topics, partitions, "
+                   "consumer groups, and clients.");
+    const std::string first_stream_name                 = "cpp-get-stats-stream-1";
+    const std::string second_stream_name                = "cpp-get-stats-stream-2";
+    const std::string first_topic_name                  = "cpp-get-stats-topic-1";
+    const std::string second_topic_name                 = "cpp-get-stats-topic-2";
+    const std::string third_topic_name                  = "cpp-get-stats-topic-3";
+    const std::string first_group_name                  = "cpp-get-stats-group-1";
+    const std::string second_group_name                 = "cpp-get-stats-group-2";
+    const std::string third_group_name                  = "cpp-get-stats-group-3";
+    constexpr std::uint32_t additional_partitions_count = 2;
+    iggy::ffi::Client *client                           = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Client *second_client = nullptr;
+    iggy::ffi::Client *third_client  = nullptr;
+
+    iggy::ffi::Stats empty_stats{};
+    ASSERT_NO_THROW({
+        empty_stats = client->get_stats();
+        EXPECT_NE(empty_stats.process_id, 0u);
+        EXPECT_GT(empty_stats.threads_count, 0u);
+        EXPECT_GT(empty_stats.total_memory, 0u);
+        EXPECT_GE(empty_stats.available_memory, 0u);
+        EXPECT_GE(empty_stats.total_disk_space, empty_stats.free_disk_space);
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.hostname).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_name).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.kernel_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.iggy_server_version).empty());
+        EXPECT_TRUE(empty_stats.iggy_server_semver == std::numeric_limits<std::uint32_t>::max() ||
+                    empty_stats.iggy_server_semver > 0u);
+        EXPECT_GE(empty_stats.cache_metrics.size(), 0u);
+        EXPECT_EQ(empty_stats.streams_count, 0u);
+        EXPECT_EQ(empty_stats.topics_count, 0u);
+        EXPECT_EQ(empty_stats.partitions_count, 0u);
+        EXPECT_EQ(empty_stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(client->create_stream(first_stream_name));
+    ASSERT_NO_THROW(client->create_stream(second_stream_name));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), first_topic_name, 1, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), second_topic_name, 2, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(second_stream_name), third_topic_name, 3, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_partitions(make_string_identifier(first_stream_name),
+                                              make_string_identifier(first_topic_name), additional_partitions_count));
+    const auto first_group  = client->create_consumer_group(make_string_identifier(first_stream_name),
+                                                            make_string_identifier(first_topic_name), first_group_name);
+    const auto second_group = client->create_consumer_group(
+        make_string_identifier(first_stream_name), make_string_identifier(second_topic_name), second_group_name);
+    const auto third_group = client->create_consumer_group(make_string_identifier(second_stream_name),
+                                                           make_string_identifier(third_topic_name), third_group_name);
+
+    ASSERT_NO_THROW({ second_client = login_to_server(); });
+    ASSERT_NE(second_client, nullptr);
+    ASSERT_NO_THROW({ third_client = login_to_server(); });
+    ASSERT_NE(third_client, nullptr);
+
+    const auto first_stream_details           = client->get_stream(make_string_identifier(first_stream_name));
+    const auto second_stream_details          = client->get_stream(make_string_identifier(second_stream_name));
+    const std::uint32_t expected_topics_count = first_stream_details.topics_count + second_stream_details.topics_count;
+    std::uint32_t first_topic_partitions      = 0;
+    std::uint32_t second_topic_partitions     = 0;
+    std::uint32_t third_topic_partitions      = 0;
+    for (const auto &topic : first_stream_details.topics) {
+        if (topic.name == first_topic_name) {
+            first_topic_partitions = topic.partitions_count;
+        }
+        if (topic.name == second_topic_name) {
+            second_topic_partitions = topic.partitions_count;
+        }
+    }
+    for (const auto &topic : second_stream_details.topics) {
+        if (topic.name == third_topic_name) {
+            third_topic_partitions = topic.partitions_count;
+        }
+    }
+    const std::uint32_t expected_partitions_count =
+        first_topic_partitions + second_topic_partitions + third_topic_partitions;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 2u);
+        EXPECT_EQ(stats.topics_count, expected_topics_count);
+        EXPECT_EQ(stats.partitions_count, expected_partitions_count);
+        EXPECT_EQ(stats.segments_count, expected_partitions_count);
+        EXPECT_EQ(stats.consumer_groups_count, 3u);
+        EXPECT_EQ(stats.clients_count, empty_stats.clients_count + 2u);
+        EXPECT_EQ(first_group.partitions_count, first_topic_partitions);
+        EXPECT_EQ(second_group.partitions_count, second_topic_partitions);
+        EXPECT_EQ(third_group.partitions_count, third_topic_partitions);
+    });
+
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(second_stream_name)));
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(first_stream_name)));
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(third_client));
+    third_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 0u);
+        EXPECT_EQ(stats.topics_count, 0u);
+        EXPECT_EQ(stats.partitions_count, 0u);
+        EXPECT_EQ(stats.segments_count, 0u);
+        EXPECT_EQ(stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetStatsIsStableAcrossBackToBackCalls) {
+    RecordProperty(
+        "description",
+        "Returns sane invariant fields across back-to-back get_stats calls on an idle authenticated client.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Stats first_stats{};
+    iggy::ffi::Stats second_stats{};
+    ASSERT_NO_THROW({
+        first_stats  = client->get_stats();
+        second_stats = client->get_stats();
+    });
+
+    EXPECT_NE(first_stats.process_id, 0u);
+    EXPECT_NE(second_stats.process_id, 0u);
+    EXPECT_EQ(second_stats.process_id, first_stats.process_id);
+    EXPECT_GT(first_stats.threads_count, 0u);
+    EXPECT_GT(second_stats.threads_count, 0u);
+    EXPECT_GT(first_stats.total_memory, 0u);
+    EXPECT_GT(second_stats.total_memory, 0u);
+    EXPECT_FALSE(static_cast<std::string>(first_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.iggy_server_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.iggy_server_version).empty());
+    EXPECT_EQ(static_cast<std::string>(second_stats.hostname), static_cast<std::string>(first_stats.hostname));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_name), static_cast<std::string>(first_stats.os_name));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_version), static_cast<std::string>(first_stats.os_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.kernel_version),
+              static_cast<std::string>(first_stats.kernel_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.iggy_server_version),
+              static_cast<std::string>(first_stats.iggy_server_version));
+    EXPECT_EQ(second_stats.iggy_server_semver, first_stats.iggy_server_semver);
+    EXPECT_EQ(second_stats.clients_count, first_stats.clients_count);
+    EXPECT_EQ(second_stats.streams_count, first_stats.streams_count);
+    EXPECT_EQ(second_stats.topics_count, first_stats.topics_count);
+    EXPECT_EQ(second_stats.partitions_count, first_stats.partitions_count);
+    EXPECT_EQ(second_stats.consumer_groups_count, first_stats.consumer_groups_count);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_me before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_me(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_me(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add additional validation for get_me after merging join_consumer_group PR.
+TEST(LowLevelE2E_Client, GetMeReturnsCurrentClientDetails) {
+    RecordProperty("description", "Returns the current authenticated client details.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_NO_THROW({
+        const auto me = client->get_me();
+        EXPECT_NE(me.client_id, 0u);
+        EXPECT_NE(me.user_id, std::numeric_limits<std::uint32_t>::max());
+        EXPECT_FALSE(static_cast<std::string>(me.address).empty());
+        EXPECT_EQ(static_cast<std::string>(me.transport), "TCP");
+        EXPECT_EQ(me.consumer_groups_count, 0u);
+        EXPECT_TRUE(me.consumer_groups.empty());
+    });
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeIsStableAcrossBackToBackCalls) {
+    RecordProperty("description", "Returns stable current-client details across back-to-back get_me calls.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({
+        first_me  = client->get_me();
+        second_me = client->get_me();
+    });
+
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_EQ(second_me.client_id, first_me.client_id);
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(second_me.address), static_cast<std::string>(first_me.address));
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), static_cast<std::string>(first_me.transport));
+    EXPECT_EQ(second_me.consumer_groups_count, first_me.consumer_groups_count);
+    EXPECT_EQ(second_me.consumer_groups.size(), first_me.consumer_groups.size());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeReturnsDistinctClientIdsForDifferentSessions) {
+    RecordProperty(
+        "description",
+        "Returns different client ids for separate authenticated sessions while keeping the same user identity.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({
+        first_me  = first_client->get_me();
+        second_me = second_client->get_me();
+    });
+
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_NE(second_me.client_id, 0u);
+    EXPECT_NE(second_me.client_id, first_me.client_id);
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeReturnsValidDetailsAfterReconnect) {
+    RecordProperty("description",
+                   "Returns valid current-client details after reconnecting with a fresh authenticated session.");
+    iggy::ffi::Client *first_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    ASSERT_NO_THROW({ first_me = first_client->get_me(); });
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_NE(first_me.user_id, std::numeric_limits<std::uint32_t>::max());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({ second_me = second_client->get_me(); });
+    EXPECT_NE(second_me.client_id, 0u);
+    EXPECT_NE(second_me.user_id, std::numeric_limits<std::uint32_t>::max());
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), static_cast<std::string>(first_me.transport));
+    EXPECT_FALSE(static_cast<std::string>(second_me.address).empty());
+    EXPECT_EQ(second_me.consumer_groups_count, 0u);
+    EXPECT_TRUE(second_me.consumer_groups.empty());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_client before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_client(1), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_client(1), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientWithWrongClientIdThrows) {
+    RecordProperty("description", "Rejects querying invalid or non-existent client ids.");
+    const std::uint32_t wrong_client_ids[] = {0u, std::numeric_limits<std::uint32_t>::max()};

Review Comment:
   now tests gets live client ids, and increments 1u if it exists (until a non-existent id is found).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3252412574


##########
foreign/cpp/include/iggy.hpp:
##########
@@ -45,6 +45,42 @@ class CompressionAlgorithm final {
     std::string algorithm_;
 };
 
+class SnapshotCompression final {

Review Comment:
   done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3161787792


##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -149,3 +153,671 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add a test to create some streams, topics, partitions, and segments, send messages, and create

Review Comment:
   stripped. I'll fix this in the next PR



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #3102:
URL: https://github.com/apache/iggy/pull/3102#issuecomment-4402291311

   This pull request has been automatically marked as stale because it has not had recent activity. It will be closed in 7 days if no further activity occurs.
   
   If you need a review, please ensure CI is green and the PR is rebased on the latest master. Don't hesitate to ping the maintainers - either @core on Discord or by mentioning them directly here on the PR.
   
   Thank you for your contribution!


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3161508057


##########
foreign/cpp/src/lib.rs:
##########
@@ -75,6 +75,69 @@ mod ffi {
         members: Vec<ConsumerGroupMember>,
     }
 
+    struct ConsumerGroupInfo {
+        stream_id: u32,
+        topic_id: u32,
+        group_id: u32,
+    }
+
+    struct ClientInfo {
+        client_id: u32,
+        user_id: u32,
+        address: String,
+        transport: String,
+        consumer_groups_count: u32,
+    }
+
+    struct ClientInfoDetails {
+        client_id: u32,
+        user_id: u32,

Review Comment:
   done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on PR #3102:
URL: https://github.com/apache/iggy/pull/3102#issuecomment-4450537545

   @hubcio i still need to completely address the review for this pr


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on PR #3102:
URL: https://github.com/apache/iggy/pull/3102#issuecomment-4466263665

   /ready


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3293220328


##########
foreign/cpp/include/iggy.hpp:
##########
@@ -26,46 +26,128 @@
 
 namespace iggy {
 
+/// Exception raised by the C++ client when an operation fails.
 class IggyException : public std::runtime_error {
   public:
     explicit IggyException(const char *message) : std::runtime_error(message) {}
     explicit IggyException(const std::string &message) : std::runtime_error(message) {}
 };
 
-class CompressionAlgorithm final {
+namespace detail {
+
+/// Internal helper used by string-backed option types in this header.
+template <typename Tag>
+class StringTag {
+  protected:
+    explicit StringTag(std::string value) : value_(std::move(value)) {}
+
+    std::string_view value() const { return value_; }
+
+  private:
+    std::string value_;
+};
+
+}  // namespace detail
+
+/// Compression setting for `Client::create_topic(...)`.
+///
+/// Use this type to choose whether messages in a topic are stored as-is or compressed with gzip.
+///
+/// Internally, this value is passed across the Rust FFI as a string.
+/// Values outside the supported set are rejected by the Rust client.
+/// Some Rust-side aliases may also be accepted for compatibility.
+class CompressionAlgorithm final : private detail::StringTag<CompressionAlgorithm> {
   public:
+    /// Store messages without compression.
     static CompressionAlgorithm none() { return CompressionAlgorithm("none"); }
+    /// Compress messages with gzip.
     static CompressionAlgorithm gzip() { return CompressionAlgorithm("gzip"); }
 
-    std::string_view compression_algorithm_value() const { return algorithm_; }
+    std::string_view compression_algorithm_value() const { return value(); }
 
   private:
-    explicit CompressionAlgorithm(std::string algorithm) : algorithm_(std::move(algorithm)) {}
-
-    std::string algorithm_;
+    explicit CompressionAlgorithm(std::string algorithm)
+        : detail::StringTag<CompressionAlgorithm>(std::move(algorithm)) {}
 };
 
-class Expiry final {
+/// Compression setting for `Client::snapshot(...)`.
+///
+/// Use this type to choose how snapshot data is compressed in the generated archive.
+///
+/// Internally, this value is passed across the Rust FFI as a string.
+/// Values outside the supported set are rejected by the Rust client.
+/// Some Rust-side aliases may also be accepted for compatibility.
+class SnapshotCompression final : private detail::StringTag<SnapshotCompression> {
   public:
-    static Expiry server_default() { return Expiry("server_default", 0); }
-    static Expiry never_expire() { return Expiry("never_expire", std::numeric_limits<std::uint64_t>::max()); }
-    static Expiry duration(std::uint64_t micros) { return Expiry("duration", micros); }
-
-    std::string_view expiry_kind() const { return expiry_kind_; }
-    std::uint64_t expiry_value() const { return expiry_value_; }
+    /// Store snapshot files without compression.
+    static SnapshotCompression stored() { return SnapshotCompression("stored"); }
+    /// Use standard deflate compression.
+    static SnapshotCompression deflated() { return SnapshotCompression("deflated"); }
+    /// Use bzip2 for a higher compression ratio at the cost of slower processing.
+    static SnapshotCompression bzip2() { return SnapshotCompression("bzip2"); }
+    /// Use Zstandard for fast compression and decompression.
+    static SnapshotCompression zstd() { return SnapshotCompression("zstd"); }
+    /// Use LZMA for high compression, especially for larger files.
+    static SnapshotCompression lzma() { return SnapshotCompression("lzma"); }
+    /// Use XZ, which is similar to LZMA and often faster to decompress.
+    static SnapshotCompression xz() { return SnapshotCompression("xz"); }
+
+    std::string_view snapshot_compression_value() const { return value(); }

Review Comment:
   `StringTag` is partly the crtp/trait type for repeated storage behaviour. On the question of also adding getters to it, I think there is not a need currently since it would reduce clarity of the API.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system, tests for purge_* functions, and consumer group functions (WIP) [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on PR #3102:
URL: https://github.com/apache/iggy/pull/3102#issuecomment-4295735981

   This PR is ready for review


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "amlel-el-mahrouss (via GitHub)" <gi...@apache.org>.
amlel-el-mahrouss commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3282494510


##########
foreign/cpp/include/iggy.hpp:
##########
@@ -26,46 +26,128 @@
 
 namespace iggy {
 
+/// Exception raised by the C++ client when an operation fails.
 class IggyException : public std::runtime_error {
   public:
     explicit IggyException(const char *message) : std::runtime_error(message) {}
     explicit IggyException(const std::string &message) : std::runtime_error(message) {}
 };
 
-class CompressionAlgorithm final {
+namespace detail {
+
+/// Internal helper used by string-backed option types in this header.
+template <typename Tag>
+class StringTag {
+  protected:
+    explicit StringTag(std::string value) : value_(std::move(value)) {}
+
+    std::string_view value() const { return value_; }
+
+  private:
+    std::string value_;
+};

Review Comment:
   `StringTag` should be marked `final` and have a destructor to satisfy the C++ ABI.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3293202188


##########
foreign/cpp/src/client.rs:
##########
@@ -21,15 +21,119 @@ use iggy::prelude::{
     ConsumerGroupClient, Identifier as RustIdentifier, IggyClient as RustIggyClient,
     IggyClientBuilder as RustIggyClientBuilder, IggyError, IggyExpiry as RustIggyExpiry,
     IggyMessage, IggyTimestamp, MaxTopicSize as RustMaxTopicSize, MessageClient, PartitionClient,
-    Partitioning, PollingStrategy, StreamClient, TopicClient, UserClient,
+    Partitioning, PollingStrategy, SnapshotCompression as RustSnapshotCompression, StreamClient,
+    SystemClient as RustSystemClient, SystemSnapshotType as RustSystemSnapshotType, TopicClient,
+    UserClient,
 };
+use iggy_common::{
+    CacheMetrics as RustCacheMetrics, CacheMetricsKey as RustCacheMetricsKey,
+    ClientInfo as RustClientInfo, ClientInfoDetails as RustClientInfoDetails, Stats as RustStats,
+};
+use std::convert::TryFrom;
 use std::str::FromStr;
 use std::sync::Arc;
 
 /// Sentinel value passed from C++ to mean "no partition specified" — the server picks the
 /// partition based on the consumer/strategy. Cxx FFI does not support `Option<u32>`, so we
 /// reserve `u32::MAX` as the sentinel for `partition_id`.
 const ANY_PARTITION_ID: u32 = u32::MAX;
+impl From<RustClientInfo> for ffi::ClientInfo {
+    fn from(client: RustClientInfo) -> Self {
+        let has_user_id = client.user_id.is_some();
+        ffi::ClientInfo {
+            client_id: client.client_id,
+            has_user_id,
+            user_id: client.user_id.unwrap_or(0),
+            address: client.address,
+            transport: client.transport,
+            consumer_groups_count: client.consumer_groups_count,
+        }
+    }
+}
+
+impl From<RustClientInfoDetails> for ffi::ClientInfoDetails {
+    fn from(client: RustClientInfoDetails) -> Self {
+        let has_user_id = client.user_id.is_some();
+        ffi::ClientInfoDetails {
+            client_id: client.client_id,
+            has_user_id,
+            user_id: client.user_id.unwrap_or(0),
+            address: client.address,
+            transport: client.transport,
+            consumer_groups_count: client.consumer_groups_count,
+            consumer_groups: client
+                .consumer_groups
+                .into_iter()
+                .map(ffi::ConsumerGroupInfo::from)
+                .collect(),
+        }
+    }
+}
+
+impl TryFrom<Option<RustClientInfoDetails>> for ffi::ClientInfoDetails {
+    type Error = String;
+
+    fn try_from(client: Option<RustClientInfoDetails>) -> Result<Self, Self::Error> {
+        match client {
+            Some(client) => Ok(ffi::ClientInfoDetails::from(client)),
+            None => Err("client not found".to_string()),
+        }
+    }
+}
+
+impl From<(RustCacheMetricsKey, RustCacheMetrics)> for ffi::CacheMetricEntry {
+    fn from((key, metrics): (RustCacheMetricsKey, RustCacheMetrics)) -> Self {
+        ffi::CacheMetricEntry {
+            stream_id: key.stream_id,
+            topic_id: key.topic_id,
+            partition_id: key.partition_id,
+            hits: metrics.hits,
+            misses: metrics.misses,
+            hit_ratio: metrics.hit_ratio,
+        }
+    }
+}
+
+impl From<RustStats> for ffi::Stats {
+    fn from(stats: RustStats) -> Self {
+        let has_server_semver = stats.iggy_server_semver.is_some();
+        ffi::Stats {
+            process_id: stats.process_id,
+            cpu_usage: stats.cpu_usage,
+            total_cpu_usage: stats.total_cpu_usage,
+            memory_usage: stats.memory_usage.as_bytes_u64(),
+            total_memory: stats.total_memory.as_bytes_u64(),
+            available_memory: stats.available_memory.as_bytes_u64(),
+            run_time: stats.run_time.as_micros(),
+            start_time: stats.start_time.as_micros(),
+            read_bytes: stats.read_bytes.as_bytes_u64(),
+            written_bytes: stats.written_bytes.as_bytes_u64(),
+            messages_size_bytes: stats.messages_size_bytes.as_bytes_u64(),
+            streams_count: stats.streams_count,
+            topics_count: stats.topics_count,
+            partitions_count: stats.partitions_count,
+            segments_count: stats.segments_count,
+            messages_count: stats.messages_count,
+            clients_count: stats.clients_count,
+            consumer_groups_count: stats.consumer_groups_count,
+            hostname: stats.hostname,
+            os_name: stats.os_name,
+            os_version: stats.os_version,
+            kernel_version: stats.kernel_version,
+            iggy_server_version: stats.iggy_server_version,
+            has_server_semver,
+            iggy_server_semver: stats.iggy_server_semver.unwrap_or(0),

Review Comment:
   documented usage of `0`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "hubcio (via GitHub)" <gi...@apache.org>.
hubcio commented on PR #3102:
URL: https://github.com/apache/iggy/pull/3102#issuecomment-4450553184

   /author


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3252441058


##########
foreign/cpp/include/iggy.hpp:
##########
@@ -45,6 +45,42 @@ class CompressionAlgorithm final {
     std::string algorithm_;
 };
 
+class SnapshotCompression final {
+  public:
+    static SnapshotCompression stored() { return SnapshotCompression("stored"); }

Review Comment:
   added a warning + documented what each of enum and its options do.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3161611701


##########
foreign/cpp/src/client.rs:
##########
@@ -378,6 +479,98 @@ impl Client {
             Ok(())
         })
     }
+
+    pub fn get_stats(&self) -> Result<ffi::Stats, String> {
+        RUNTIME.block_on(async {
+            let stats = self
+                .inner
+                .get_stats()
+                .await
+                .map_err(|error| format!("Could not get stats: {error}"))?;
+            Ok(ffi::Stats::from(stats))
+        })
+    }
+
+    pub fn get_me(&self) -> Result<ffi::ClientInfoDetails, String> {
+        RUNTIME.block_on(async {
+            let client = self
+                .inner
+                .get_me()
+                .await
+                .map_err(|error| format!("Could not get current client info: {error}"))?;
+            Ok(ffi::ClientInfoDetails::from(client))
+        })
+    }
+
+    pub fn get_client(&self, client_id: u32) -> Result<ffi::ClientInfoDetails, String> {
+        RUNTIME.block_on(async {
+            let client = self
+                .inner
+                .get_client(client_id)
+                .await
+                .map_err(|error| format!("Could not get client '{client_id}': {error}"))?;
+            ffi::ClientInfoDetails::try_from(client)
+                .map_err(|error| format!("Could not get client '{client_id}': {error}"))
+        })
+    }
+
+    pub fn get_clients(&self) -> Result<Vec<ffi::ClientInfo>, String> {
+        RUNTIME.block_on(async {
+            let clients = self
+                .inner
+                .get_clients()
+                .await
+                .map_err(|error| format!("Could not get clients: {error}"))?;
+            Ok(clients.into_iter().map(ffi::ClientInfo::from).collect())
+        })
+    }
+
+    pub fn ping(&self) -> Result<(), String> {
+        RUNTIME.block_on(async {
+            self.inner
+                .ping()
+                .await
+                .map_err(|error| format!("Could not ping server: {error}"))?;
+            Ok(())
+        })
+    }
+
+    pub fn heartbeat_interval(&self) -> u64 {
+        RUNTIME.block_on(async { self.inner.heartbeat_interval().await.as_micros() })
+    }
+
+    pub fn snapshot(
+        &self,
+        snapshot_compression: String,
+        snapshot_types: Vec<String>,
+    ) -> Result<Vec<u8>, String> {
+        let rust_compression = match snapshot_compression.trim() {
+            "" => RustSnapshotCompression::default(),
+            value => RustSnapshotCompression::from_str(value).map_err(|error| {
+                format!("Could not capture snapshot: invalid compression '{value}': {error}")
+            })?,
+        };
+        let rust_snapshot_types = snapshot_types
+            .into_iter()
+            .map(|snapshot_type| {
+                RustSystemSnapshotType::from_str(&snapshot_type).map_err(|error| {
+                    format!(
+                        "Could not capture snapshot: invalid snapshot type '{}': {}",

Review Comment:
   `cargo clippy -- -W clippy::uninlined_format_args` caught a lot more errors than this -- fixed them all



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3161763106


##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -149,3 +153,671 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add a test to create some streams, topics, partitions, and segments, send messages, and create
+// consumer groups and verify it.
+TEST(LowLevelE2E_Client, GetStatsReturnsServerStats) {
+    RecordProperty("description",
+                   "Returns empty resource counts first, then reflects aggregated streams, topics, partitions, "
+                   "consumer groups, and clients.");
+    const std::string first_stream_name                 = "cpp-get-stats-stream-1";
+    const std::string second_stream_name                = "cpp-get-stats-stream-2";
+    const std::string first_topic_name                  = "cpp-get-stats-topic-1";
+    const std::string second_topic_name                 = "cpp-get-stats-topic-2";
+    const std::string third_topic_name                  = "cpp-get-stats-topic-3";
+    const std::string first_group_name                  = "cpp-get-stats-group-1";
+    const std::string second_group_name                 = "cpp-get-stats-group-2";
+    const std::string third_group_name                  = "cpp-get-stats-group-3";
+    constexpr std::uint32_t additional_partitions_count = 2;
+    iggy::ffi::Client *client                           = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Client *second_client = nullptr;
+    iggy::ffi::Client *third_client  = nullptr;
+
+    iggy::ffi::Stats empty_stats{};
+    ASSERT_NO_THROW({
+        empty_stats = client->get_stats();
+        EXPECT_NE(empty_stats.process_id, 0u);
+        EXPECT_GT(empty_stats.threads_count, 0u);
+        EXPECT_GT(empty_stats.total_memory, 0u);
+        EXPECT_GE(empty_stats.available_memory, 0u);
+        EXPECT_GE(empty_stats.total_disk_space, empty_stats.free_disk_space);
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.hostname).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_name).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.kernel_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.iggy_server_version).empty());
+        EXPECT_TRUE(empty_stats.iggy_server_semver == std::numeric_limits<std::uint32_t>::max() ||
+                    empty_stats.iggy_server_semver > 0u);
+        EXPECT_GE(empty_stats.cache_metrics.size(), 0u);
+        EXPECT_EQ(empty_stats.streams_count, 0u);
+        EXPECT_EQ(empty_stats.topics_count, 0u);
+        EXPECT_EQ(empty_stats.partitions_count, 0u);
+        EXPECT_EQ(empty_stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(client->create_stream(first_stream_name));
+    ASSERT_NO_THROW(client->create_stream(second_stream_name));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), first_topic_name, 1, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), second_topic_name, 2, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(second_stream_name), third_topic_name, 3, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_partitions(make_string_identifier(first_stream_name),
+                                              make_string_identifier(first_topic_name), additional_partitions_count));
+    const auto first_group  = client->create_consumer_group(make_string_identifier(first_stream_name),
+                                                            make_string_identifier(first_topic_name), first_group_name);
+    const auto second_group = client->create_consumer_group(
+        make_string_identifier(first_stream_name), make_string_identifier(second_topic_name), second_group_name);
+    const auto third_group = client->create_consumer_group(make_string_identifier(second_stream_name),
+                                                           make_string_identifier(third_topic_name), third_group_name);
+
+    ASSERT_NO_THROW({ second_client = login_to_server(); });
+    ASSERT_NE(second_client, nullptr);
+    ASSERT_NO_THROW({ third_client = login_to_server(); });
+    ASSERT_NE(third_client, nullptr);
+
+    const auto first_stream_details           = client->get_stream(make_string_identifier(first_stream_name));
+    const auto second_stream_details          = client->get_stream(make_string_identifier(second_stream_name));
+    const std::uint32_t expected_topics_count = first_stream_details.topics_count + second_stream_details.topics_count;
+    std::uint32_t first_topic_partitions      = 0;
+    std::uint32_t second_topic_partitions     = 0;
+    std::uint32_t third_topic_partitions      = 0;
+    for (const auto &topic : first_stream_details.topics) {
+        if (topic.name == first_topic_name) {
+            first_topic_partitions = topic.partitions_count;
+        }
+        if (topic.name == second_topic_name) {
+            second_topic_partitions = topic.partitions_count;
+        }
+    }
+    for (const auto &topic : second_stream_details.topics) {
+        if (topic.name == third_topic_name) {
+            third_topic_partitions = topic.partitions_count;
+        }
+    }
+    const std::uint32_t expected_partitions_count =
+        first_topic_partitions + second_topic_partitions + third_topic_partitions;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 2u);
+        EXPECT_EQ(stats.topics_count, expected_topics_count);
+        EXPECT_EQ(stats.partitions_count, expected_partitions_count);
+        EXPECT_EQ(stats.segments_count, expected_partitions_count);
+        EXPECT_EQ(stats.consumer_groups_count, 3u);
+        EXPECT_EQ(stats.clients_count, empty_stats.clients_count + 2u);
+        EXPECT_EQ(first_group.partitions_count, first_topic_partitions);
+        EXPECT_EQ(second_group.partitions_count, second_topic_partitions);
+        EXPECT_EQ(third_group.partitions_count, third_topic_partitions);
+    });
+
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(second_stream_name)));
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(first_stream_name)));
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(third_client));
+    third_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 0u);
+        EXPECT_EQ(stats.topics_count, 0u);
+        EXPECT_EQ(stats.partitions_count, 0u);
+        EXPECT_EQ(stats.segments_count, 0u);
+        EXPECT_EQ(stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetStatsIsStableAcrossBackToBackCalls) {
+    RecordProperty(
+        "description",
+        "Returns sane invariant fields across back-to-back get_stats calls on an idle authenticated client.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Stats first_stats{};
+    iggy::ffi::Stats second_stats{};
+    ASSERT_NO_THROW({
+        first_stats  = client->get_stats();
+        second_stats = client->get_stats();
+    });
+
+    EXPECT_NE(first_stats.process_id, 0u);
+    EXPECT_NE(second_stats.process_id, 0u);
+    EXPECT_EQ(second_stats.process_id, first_stats.process_id);
+    EXPECT_GT(first_stats.threads_count, 0u);
+    EXPECT_GT(second_stats.threads_count, 0u);
+    EXPECT_GT(first_stats.total_memory, 0u);
+    EXPECT_GT(second_stats.total_memory, 0u);
+    EXPECT_FALSE(static_cast<std::string>(first_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.iggy_server_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.iggy_server_version).empty());
+    EXPECT_EQ(static_cast<std::string>(second_stats.hostname), static_cast<std::string>(first_stats.hostname));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_name), static_cast<std::string>(first_stats.os_name));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_version), static_cast<std::string>(first_stats.os_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.kernel_version),
+              static_cast<std::string>(first_stats.kernel_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.iggy_server_version),
+              static_cast<std::string>(first_stats.iggy_server_version));
+    EXPECT_EQ(second_stats.iggy_server_semver, first_stats.iggy_server_semver);
+    EXPECT_EQ(second_stats.clients_count, first_stats.clients_count);
+    EXPECT_EQ(second_stats.streams_count, first_stats.streams_count);
+    EXPECT_EQ(second_stats.topics_count, first_stats.topics_count);
+    EXPECT_EQ(second_stats.partitions_count, first_stats.partitions_count);
+    EXPECT_EQ(second_stats.consumer_groups_count, first_stats.consumer_groups_count);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_me before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_me(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_me(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add additional validation for get_me after merging join_consumer_group PR.
+TEST(LowLevelE2E_Client, GetMeReturnsCurrentClientDetails) {
+    RecordProperty("description", "Returns the current authenticated client details.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_NO_THROW({
+        const auto me = client->get_me();
+        EXPECT_NE(me.client_id, 0u);
+        EXPECT_NE(me.user_id, std::numeric_limits<std::uint32_t>::max());
+        EXPECT_FALSE(static_cast<std::string>(me.address).empty());
+        EXPECT_EQ(static_cast<std::string>(me.transport), "TCP");
+        EXPECT_EQ(me.consumer_groups_count, 0u);
+        EXPECT_TRUE(me.consumer_groups.empty());
+    });
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeIsStableAcrossBackToBackCalls) {
+    RecordProperty("description", "Returns stable current-client details across back-to-back get_me calls.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({
+        first_me  = client->get_me();
+        second_me = client->get_me();
+    });
+
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_EQ(second_me.client_id, first_me.client_id);
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(second_me.address), static_cast<std::string>(first_me.address));
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), static_cast<std::string>(first_me.transport));
+    EXPECT_EQ(second_me.consumer_groups_count, first_me.consumer_groups_count);
+    EXPECT_EQ(second_me.consumer_groups.size(), first_me.consumer_groups.size());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeReturnsDistinctClientIdsForDifferentSessions) {
+    RecordProperty(
+        "description",
+        "Returns different client ids for separate authenticated sessions while keeping the same user identity.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({
+        first_me  = first_client->get_me();
+        second_me = second_client->get_me();
+    });
+
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_NE(second_me.client_id, 0u);
+    EXPECT_NE(second_me.client_id, first_me.client_id);
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeReturnsValidDetailsAfterReconnect) {
+    RecordProperty("description",
+                   "Returns valid current-client details after reconnecting with a fresh authenticated session.");
+    iggy::ffi::Client *first_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    ASSERT_NO_THROW({ first_me = first_client->get_me(); });
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_NE(first_me.user_id, std::numeric_limits<std::uint32_t>::max());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({ second_me = second_client->get_me(); });
+    EXPECT_NE(second_me.client_id, 0u);
+    EXPECT_NE(second_me.user_id, std::numeric_limits<std::uint32_t>::max());
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), static_cast<std::string>(first_me.transport));
+    EXPECT_FALSE(static_cast<std::string>(second_me.address).empty());
+    EXPECT_EQ(second_me.consumer_groups_count, 0u);
+    EXPECT_TRUE(second_me.consumer_groups.empty());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_client before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_client(1), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_client(1), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientWithWrongClientIdThrows) {
+    RecordProperty("description", "Rejects querying invalid or non-existent client ids.");
+    const std::uint32_t wrong_client_ids[] = {0u, std::numeric_limits<std::uint32_t>::max()};
+    iggy::ffi::Client *client              = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    for (const std::uint32_t wrong_client_id : wrong_client_ids) {
+        SCOPED_TRACE(wrong_client_id);
+        ASSERT_THROW(client->get_client(wrong_client_id), std::exception);
+    }
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientReturnsDetailsForMatchingClientId) {
+    RecordProperty("description", "Returns current client details when querying with the authenticated client id.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::ClientInfoDetails current_client{};
+    iggy::ffi::ClientInfoDetails looked_up_client{};
+    ASSERT_NO_THROW({
+        current_client   = client->get_me();
+        looked_up_client = client->get_client(current_client.client_id);
+    });
+
+    EXPECT_NE(current_client.client_id, 0u);
+    EXPECT_EQ(looked_up_client.client_id, current_client.client_id);
+    EXPECT_EQ(looked_up_client.user_id, current_client.user_id);
+    EXPECT_EQ(static_cast<std::string>(looked_up_client.address), static_cast<std::string>(current_client.address));
+    EXPECT_EQ(static_cast<std::string>(looked_up_client.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(looked_up_client.transport), static_cast<std::string>(current_client.transport));
+    EXPECT_EQ(looked_up_client.consumer_groups_count, current_client.consumer_groups_count);
+    EXPECT_EQ(looked_up_client.consumer_groups.size(), current_client.consumer_groups.size());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientIsStableAcrossBackToBackCalls) {
+    RecordProperty("description", "Returns stable client details across back-to-back get_client calls.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::ClientInfoDetails current_client{};
+    iggy::ffi::ClientInfoDetails first_lookup{};
+    iggy::ffi::ClientInfoDetails second_lookup{};
+    ASSERT_NO_THROW({
+        current_client = client->get_me();
+        first_lookup   = client->get_client(current_client.client_id);
+        second_lookup  = client->get_client(current_client.client_id);
+    });
+
+    EXPECT_NE(current_client.client_id, 0u);
+    EXPECT_EQ(first_lookup.client_id, current_client.client_id);
+    EXPECT_EQ(second_lookup.client_id, first_lookup.client_id);
+    EXPECT_EQ(second_lookup.user_id, first_lookup.user_id);
+    EXPECT_EQ(static_cast<std::string>(second_lookup.address), static_cast<std::string>(first_lookup.address));
+    EXPECT_EQ(static_cast<std::string>(first_lookup.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_lookup.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_lookup.transport), static_cast<std::string>(first_lookup.transport));
+    EXPECT_EQ(second_lookup.consumer_groups_count, first_lookup.consumer_groups_count);
+    EXPECT_EQ(second_lookup.consumer_groups.size(), first_lookup.consumer_groups.size());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_clients before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_clients(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_clients(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsReturnsActiveClientSessions) {
+    RecordProperty("description", "Returns the currently active authenticated client sessions.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails second_me{};
+    rust::Vec<iggy::ffi::ClientInfo> clients;
+    ASSERT_NO_THROW({
+        first_me  = first_client->get_me();
+        second_me = second_client->get_me();
+        clients   = first_client->get_clients();
+    });
+
+    ASSERT_GE(clients.size(), 2u);
+
+    bool found_first  = false;
+    bool found_second = false;
+    for (const auto &client : clients) {
+        EXPECT_NE(client.client_id, 0u);
+        EXPECT_EQ(static_cast<std::string>(client.transport), "TCP");
+
+        if (client.client_id == first_me.client_id) {
+            found_first = true;
+            EXPECT_EQ(client.user_id, first_me.user_id);
+            EXPECT_EQ(static_cast<std::string>(client.address), static_cast<std::string>(first_me.address));
+            EXPECT_EQ(client.consumer_groups_count, first_me.consumer_groups_count);
+        }
+
+        if (client.client_id == second_me.client_id) {
+            found_second = true;
+            EXPECT_EQ(client.user_id, second_me.user_id);
+            EXPECT_EQ(static_cast<std::string>(client.address), static_cast<std::string>(second_me.address));
+            EXPECT_EQ(client.consumer_groups_count, second_me.consumer_groups_count);
+        }
+    }
+
+    EXPECT_TRUE(found_first);
+    EXPECT_TRUE(found_second);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsIsStableAcrossBackToBackCalls) {
+    RecordProperty("description", "Returns stable client lists across back-to-back get_clients calls.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    rust::Vec<iggy::ffi::ClientInfo> first_clients;
+    rust::Vec<iggy::ffi::ClientInfo> second_clients;
+    ASSERT_NO_THROW({
+        first_clients  = first_client->get_clients();
+        second_clients = first_client->get_clients();
+    });
+
+    ASSERT_EQ(second_clients.size(), first_clients.size());
+    for (const auto &first_entry : first_clients) {
+        bool found_match = false;
+        for (const auto &second_entry : second_clients) {
+            if (second_entry.client_id != first_entry.client_id) {
+                continue;
+            }
+
+            found_match = true;
+            EXPECT_EQ(second_entry.user_id, first_entry.user_id);
+            EXPECT_EQ(static_cast<std::string>(second_entry.address), static_cast<std::string>(first_entry.address));
+            EXPECT_EQ(static_cast<std::string>(second_entry.transport),
+                      static_cast<std::string>(first_entry.transport));
+            EXPECT_EQ(second_entry.consumer_groups_count, first_entry.consumer_groups_count);
+            break;
+        }
+        EXPECT_TRUE(found_match);
+    }
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsMatchesGetClientForReturnedIds) {
+    RecordProperty("description", "Returns list entries that agree with get_client for each returned client id.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    rust::Vec<iggy::ffi::ClientInfo> clients;
+    ASSERT_NO_THROW({ clients = first_client->get_clients(); });
+    ASSERT_GE(clients.size(), 2u);
+
+    for (const auto &client : clients) {
+        SCOPED_TRACE(client.client_id);
+        iggy::ffi::ClientInfoDetails details{};
+        ASSERT_NO_THROW({ details = first_client->get_client(client.client_id); });
+
+        EXPECT_EQ(details.client_id, client.client_id);
+        EXPECT_EQ(details.user_id, client.user_id);
+        EXPECT_EQ(static_cast<std::string>(details.address), static_cast<std::string>(client.address));
+        EXPECT_EQ(static_cast<std::string>(details.transport), static_cast<std::string>(client.transport));
+        EXPECT_EQ(details.consumer_groups_count, client.consumer_groups_count);
+    }
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsReflectsAdditionalSession) {
+    RecordProperty("description", "Reflects a newly added authenticated session in subsequent get_clients results.");
+    iggy::ffi::Client *first_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+
+    rust::Vec<iggy::ffi::ClientInfo> clients_before;
+    ASSERT_NO_THROW({ clients_before = first_client->get_clients(); });
+
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails second_me{};
+    rust::Vec<iggy::ffi::ClientInfo> clients_after;
+    ASSERT_NO_THROW({
+        second_me     = second_client->get_me();
+        clients_after = first_client->get_clients();
+    });
+
+    bool found_before = false;
+    for (const auto &client : clients_before) {
+        if (client.client_id == second_me.client_id) {
+            found_before = true;
+            break;
+        }
+    }
+    EXPECT_FALSE(found_before);
+
+    bool found_after = false;
+    for (const auto &client : clients_after) {
+        if (client.client_id != second_me.client_id) {
+            continue;
+        }
+
+        found_after = true;
+        EXPECT_EQ(client.user_id, second_me.user_id);
+        EXPECT_EQ(static_cast<std::string>(client.address), static_cast<std::string>(second_me.address));
+        EXPECT_EQ(static_cast<std::string>(client.transport), "TCP");
+        EXPECT_EQ(client.consumer_groups_count, second_me.consumer_groups_count);
+        break;
+    }
+    EXPECT_TRUE(found_after);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, PingSucceedsForNewConnection) {
+    RecordProperty("description", "Successfully pings the server from a fresh unauthenticated client session.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_NO_THROW(client->ping());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, HeartbeatIntervalReturnsDefaultValueForNewConnection) {
+    RecordProperty("description",
+                   "Returns the default heartbeat interval in microseconds for a fresh unauthenticated client.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    const auto heartbeat_interval = client->heartbeat_interval();
+    EXPECT_EQ(heartbeat_interval, 5'000'000u);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, HeartbeatIntervalReturnsConfiguredValueFromConnectionString) {
+    RecordProperty("description",
+                   "Returns the configured heartbeat interval in microseconds from the connection string.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection("iggy://iggy:iggy@127.0.0.1:8090?heartbeat_interval=10s"); });
+    ASSERT_NE(client, nullptr);
+
+    const auto heartbeat_interval = client->heartbeat_interval();
+    EXPECT_EQ(heartbeat_interval, 10'000'000u);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, SnapshotBeforeLoginThrows) {

Review Comment:
   done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3252447844


##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -149,3 +153,671 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add a test to create some streams, topics, partitions, and segments, send messages, and create
+// consumer groups and verify it.
+TEST(LowLevelE2E_Client, GetStatsReturnsServerStats) {
+    RecordProperty("description",
+                   "Returns empty resource counts first, then reflects aggregated streams, topics, partitions, "
+                   "consumer groups, and clients.");
+    const std::string first_stream_name                 = "cpp-get-stats-stream-1";
+    const std::string second_stream_name                = "cpp-get-stats-stream-2";
+    const std::string first_topic_name                  = "cpp-get-stats-topic-1";
+    const std::string second_topic_name                 = "cpp-get-stats-topic-2";
+    const std::string third_topic_name                  = "cpp-get-stats-topic-3";
+    const std::string first_group_name                  = "cpp-get-stats-group-1";
+    const std::string second_group_name                 = "cpp-get-stats-group-2";
+    const std::string third_group_name                  = "cpp-get-stats-group-3";
+    constexpr std::uint32_t additional_partitions_count = 2;
+    iggy::ffi::Client *client                           = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Client *second_client = nullptr;
+    iggy::ffi::Client *third_client  = nullptr;
+
+    iggy::ffi::Stats empty_stats{};
+    ASSERT_NO_THROW({
+        empty_stats = client->get_stats();
+        EXPECT_NE(empty_stats.process_id, 0u);
+        EXPECT_GT(empty_stats.threads_count, 0u);
+        EXPECT_GT(empty_stats.total_memory, 0u);
+        EXPECT_GE(empty_stats.available_memory, 0u);

Review Comment:
   done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "hubcio (via GitHub)" <gi...@apache.org>.
hubcio commented on PR #3102:
URL: https://github.com/apache/iggy/pull/3102#issuecomment-4450548248

   @slbotbm I know. `/ready` is me marking the PR ready for review. see `CONTRIBUTING.md` - i added simple triage bot. this PR was created before the bot, so I'm marking manually all existing PRs.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "hubcio (via GitHub)" <gi...@apache.org>.
hubcio commented on PR #3102:
URL: https://github.com/apache/iggy/pull/3102#issuecomment-4450527043

   /ready


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3293208358


##########
foreign/cpp/include/iggy.hpp:
##########
@@ -26,46 +26,128 @@
 
 namespace iggy {
 
+/// Exception raised by the C++ client when an operation fails.
 class IggyException : public std::runtime_error {
   public:
     explicit IggyException(const char *message) : std::runtime_error(message) {}
     explicit IggyException(const std::string &message) : std::runtime_error(message) {}
 };
 
-class CompressionAlgorithm final {
+namespace detail {
+
+/// Internal helper used by string-backed option types in this header.
+template <typename Tag>
+class StringTag {
+  protected:
+    explicit StringTag(std::string value) : value_(std::move(value)) {}
+
+    std::string_view value() const { return value_; }
+
+  private:
+    std::string value_;
+};

Review Comment:
   marking `StringTag` as `final` would make the rest of the public wrapper invalid. The wrapper types have final
   
   Add destructor.



##########
foreign/cpp/include/iggy.hpp:
##########
@@ -26,46 +26,128 @@
 
 namespace iggy {
 
+/// Exception raised by the C++ client when an operation fails.
 class IggyException : public std::runtime_error {
   public:
     explicit IggyException(const char *message) : std::runtime_error(message) {}
     explicit IggyException(const std::string &message) : std::runtime_error(message) {}
 };
 
-class CompressionAlgorithm final {
+namespace detail {
+
+/// Internal helper used by string-backed option types in this header.
+template <typename Tag>
+class StringTag {
+  protected:
+    explicit StringTag(std::string value) : value_(std::move(value)) {}
+
+    std::string_view value() const { return value_; }
+
+  private:
+    std::string value_;
+};

Review Comment:
   marking `StringTag` as `final` would make the rest of the public wrapper invalid. The wrapper types have final
   
   Added destructor.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3161336968


##########
foreign/cpp/build.rs:
##########
@@ -25,5 +25,6 @@ fn main() {
     println!("cargo:rerun-if-changed=src/identifier.rs");
     println!("cargo:rerun-if-changed=src/lib.rs");
     println!("cargo:rerun-if-changed=src/stream.rs");
+    println!("cargo:rerun-if-changed=src/system.rs");

Review Comment:
   deleted line



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3161792611


##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -149,3 +153,671 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add a test to create some streams, topics, partitions, and segments, send messages, and create
+// consumer groups and verify it.
+TEST(LowLevelE2E_Client, GetStatsReturnsServerStats) {
+    RecordProperty("description",
+                   "Returns empty resource counts first, then reflects aggregated streams, topics, partitions, "
+                   "consumer groups, and clients.");
+    const std::string first_stream_name                 = "cpp-get-stats-stream-1";
+    const std::string second_stream_name                = "cpp-get-stats-stream-2";
+    const std::string first_topic_name                  = "cpp-get-stats-topic-1";
+    const std::string second_topic_name                 = "cpp-get-stats-topic-2";
+    const std::string third_topic_name                  = "cpp-get-stats-topic-3";
+    const std::string first_group_name                  = "cpp-get-stats-group-1";
+    const std::string second_group_name                 = "cpp-get-stats-group-2";
+    const std::string third_group_name                  = "cpp-get-stats-group-3";
+    constexpr std::uint32_t additional_partitions_count = 2;
+    iggy::ffi::Client *client                           = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Client *second_client = nullptr;
+    iggy::ffi::Client *third_client  = nullptr;
+
+    iggy::ffi::Stats empty_stats{};
+    ASSERT_NO_THROW({
+        empty_stats = client->get_stats();
+        EXPECT_NE(empty_stats.process_id, 0u);
+        EXPECT_GT(empty_stats.threads_count, 0u);
+        EXPECT_GT(empty_stats.total_memory, 0u);
+        EXPECT_GE(empty_stats.available_memory, 0u);
+        EXPECT_GE(empty_stats.total_disk_space, empty_stats.free_disk_space);
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.hostname).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_name).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.kernel_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.iggy_server_version).empty());
+        EXPECT_TRUE(empty_stats.iggy_server_semver == std::numeric_limits<std::uint32_t>::max() ||
+                    empty_stats.iggy_server_semver > 0u);
+        EXPECT_GE(empty_stats.cache_metrics.size(), 0u);

Review Comment:
   done



##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -149,3 +153,671 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add a test to create some streams, topics, partitions, and segments, send messages, and create
+// consumer groups and verify it.
+TEST(LowLevelE2E_Client, GetStatsReturnsServerStats) {
+    RecordProperty("description",
+                   "Returns empty resource counts first, then reflects aggregated streams, topics, partitions, "
+                   "consumer groups, and clients.");
+    const std::string first_stream_name                 = "cpp-get-stats-stream-1";
+    const std::string second_stream_name                = "cpp-get-stats-stream-2";
+    const std::string first_topic_name                  = "cpp-get-stats-topic-1";
+    const std::string second_topic_name                 = "cpp-get-stats-topic-2";
+    const std::string third_topic_name                  = "cpp-get-stats-topic-3";
+    const std::string first_group_name                  = "cpp-get-stats-group-1";
+    const std::string second_group_name                 = "cpp-get-stats-group-2";
+    const std::string third_group_name                  = "cpp-get-stats-group-3";
+    constexpr std::uint32_t additional_partitions_count = 2;
+    iggy::ffi::Client *client                           = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Client *second_client = nullptr;
+    iggy::ffi::Client *third_client  = nullptr;
+
+    iggy::ffi::Stats empty_stats{};
+    ASSERT_NO_THROW({
+        empty_stats = client->get_stats();
+        EXPECT_NE(empty_stats.process_id, 0u);
+        EXPECT_GT(empty_stats.threads_count, 0u);
+        EXPECT_GT(empty_stats.total_memory, 0u);
+        EXPECT_GE(empty_stats.available_memory, 0u);
+        EXPECT_GE(empty_stats.total_disk_space, empty_stats.free_disk_space);
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.hostname).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_name).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.kernel_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.iggy_server_version).empty());
+        EXPECT_TRUE(empty_stats.iggy_server_semver == std::numeric_limits<std::uint32_t>::max() ||
+                    empty_stats.iggy_server_semver > 0u);
+        EXPECT_GE(empty_stats.cache_metrics.size(), 0u);
+        EXPECT_EQ(empty_stats.streams_count, 0u);
+        EXPECT_EQ(empty_stats.topics_count, 0u);
+        EXPECT_EQ(empty_stats.partitions_count, 0u);
+        EXPECT_EQ(empty_stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(client->create_stream(first_stream_name));
+    ASSERT_NO_THROW(client->create_stream(second_stream_name));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), first_topic_name, 1, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), second_topic_name, 2, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(second_stream_name), third_topic_name, 3, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_partitions(make_string_identifier(first_stream_name),
+                                              make_string_identifier(first_topic_name), additional_partitions_count));
+    const auto first_group  = client->create_consumer_group(make_string_identifier(first_stream_name),
+                                                            make_string_identifier(first_topic_name), first_group_name);
+    const auto second_group = client->create_consumer_group(
+        make_string_identifier(first_stream_name), make_string_identifier(second_topic_name), second_group_name);
+    const auto third_group = client->create_consumer_group(make_string_identifier(second_stream_name),
+                                                           make_string_identifier(third_topic_name), third_group_name);
+
+    ASSERT_NO_THROW({ second_client = login_to_server(); });
+    ASSERT_NE(second_client, nullptr);
+    ASSERT_NO_THROW({ third_client = login_to_server(); });
+    ASSERT_NE(third_client, nullptr);
+
+    const auto first_stream_details           = client->get_stream(make_string_identifier(first_stream_name));
+    const auto second_stream_details          = client->get_stream(make_string_identifier(second_stream_name));
+    const std::uint32_t expected_topics_count = first_stream_details.topics_count + second_stream_details.topics_count;
+    std::uint32_t first_topic_partitions      = 0;
+    std::uint32_t second_topic_partitions     = 0;
+    std::uint32_t third_topic_partitions      = 0;
+    for (const auto &topic : first_stream_details.topics) {
+        if (topic.name == first_topic_name) {
+            first_topic_partitions = topic.partitions_count;
+        }
+        if (topic.name == second_topic_name) {
+            second_topic_partitions = topic.partitions_count;
+        }
+    }
+    for (const auto &topic : second_stream_details.topics) {
+        if (topic.name == third_topic_name) {
+            third_topic_partitions = topic.partitions_count;
+        }
+    }
+    const std::uint32_t expected_partitions_count =
+        first_topic_partitions + second_topic_partitions + third_topic_partitions;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 2u);
+        EXPECT_EQ(stats.topics_count, expected_topics_count);
+        EXPECT_EQ(stats.partitions_count, expected_partitions_count);
+        EXPECT_EQ(stats.segments_count, expected_partitions_count);
+        EXPECT_EQ(stats.consumer_groups_count, 3u);
+        EXPECT_EQ(stats.clients_count, empty_stats.clients_count + 2u);
+        EXPECT_EQ(first_group.partitions_count, first_topic_partitions);
+        EXPECT_EQ(second_group.partitions_count, second_topic_partitions);
+        EXPECT_EQ(third_group.partitions_count, third_topic_partitions);
+    });
+
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(second_stream_name)));
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(first_stream_name)));
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(third_client));
+    third_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 0u);
+        EXPECT_EQ(stats.topics_count, 0u);
+        EXPECT_EQ(stats.partitions_count, 0u);
+        EXPECT_EQ(stats.segments_count, 0u);
+        EXPECT_EQ(stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetStatsIsStableAcrossBackToBackCalls) {
+    RecordProperty(
+        "description",
+        "Returns sane invariant fields across back-to-back get_stats calls on an idle authenticated client.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Stats first_stats{};
+    iggy::ffi::Stats second_stats{};
+    ASSERT_NO_THROW({
+        first_stats  = client->get_stats();
+        second_stats = client->get_stats();
+    });
+
+    EXPECT_NE(first_stats.process_id, 0u);
+    EXPECT_NE(second_stats.process_id, 0u);
+    EXPECT_EQ(second_stats.process_id, first_stats.process_id);
+    EXPECT_GT(first_stats.threads_count, 0u);
+    EXPECT_GT(second_stats.threads_count, 0u);
+    EXPECT_GT(first_stats.total_memory, 0u);
+    EXPECT_GT(second_stats.total_memory, 0u);
+    EXPECT_FALSE(static_cast<std::string>(first_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.iggy_server_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.iggy_server_version).empty());
+    EXPECT_EQ(static_cast<std::string>(second_stats.hostname), static_cast<std::string>(first_stats.hostname));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_name), static_cast<std::string>(first_stats.os_name));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_version), static_cast<std::string>(first_stats.os_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.kernel_version),
+              static_cast<std::string>(first_stats.kernel_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.iggy_server_version),
+              static_cast<std::string>(first_stats.iggy_server_version));
+    EXPECT_EQ(second_stats.iggy_server_semver, first_stats.iggy_server_semver);
+    EXPECT_EQ(second_stats.clients_count, first_stats.clients_count);
+    EXPECT_EQ(second_stats.streams_count, first_stats.streams_count);
+    EXPECT_EQ(second_stats.topics_count, first_stats.topics_count);
+    EXPECT_EQ(second_stats.partitions_count, first_stats.partitions_count);
+    EXPECT_EQ(second_stats.consumer_groups_count, first_stats.consumer_groups_count);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_me before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_me(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_me(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add additional validation for get_me after merging join_consumer_group PR.

Review Comment:
   done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3252451548


##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -149,3 +153,671 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add a test to create some streams, topics, partitions, and segments, send messages, and create
+// consumer groups and verify it.
+TEST(LowLevelE2E_Client, GetStatsReturnsServerStats) {
+    RecordProperty("description",
+                   "Returns empty resource counts first, then reflects aggregated streams, topics, partitions, "
+                   "consumer groups, and clients.");
+    const std::string first_stream_name                 = "cpp-get-stats-stream-1";
+    const std::string second_stream_name                = "cpp-get-stats-stream-2";
+    const std::string first_topic_name                  = "cpp-get-stats-topic-1";
+    const std::string second_topic_name                 = "cpp-get-stats-topic-2";
+    const std::string third_topic_name                  = "cpp-get-stats-topic-3";
+    const std::string first_group_name                  = "cpp-get-stats-group-1";
+    const std::string second_group_name                 = "cpp-get-stats-group-2";
+    const std::string third_group_name                  = "cpp-get-stats-group-3";
+    constexpr std::uint32_t additional_partitions_count = 2;
+    iggy::ffi::Client *client                           = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Client *second_client = nullptr;
+    iggy::ffi::Client *third_client  = nullptr;
+
+    iggy::ffi::Stats empty_stats{};
+    ASSERT_NO_THROW({
+        empty_stats = client->get_stats();
+        EXPECT_NE(empty_stats.process_id, 0u);
+        EXPECT_GT(empty_stats.threads_count, 0u);
+        EXPECT_GT(empty_stats.total_memory, 0u);
+        EXPECT_GE(empty_stats.available_memory, 0u);
+        EXPECT_GE(empty_stats.total_disk_space, empty_stats.free_disk_space);
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.hostname).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_name).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.kernel_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.iggy_server_version).empty());
+        EXPECT_TRUE(empty_stats.iggy_server_semver == std::numeric_limits<std::uint32_t>::max() ||
+                    empty_stats.iggy_server_semver > 0u);
+        EXPECT_GE(empty_stats.cache_metrics.size(), 0u);
+        EXPECT_EQ(empty_stats.streams_count, 0u);
+        EXPECT_EQ(empty_stats.topics_count, 0u);
+        EXPECT_EQ(empty_stats.partitions_count, 0u);
+        EXPECT_EQ(empty_stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(client->create_stream(first_stream_name));
+    ASSERT_NO_THROW(client->create_stream(second_stream_name));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), first_topic_name, 1, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), second_topic_name, 2, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(second_stream_name), third_topic_name, 3, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_partitions(make_string_identifier(first_stream_name),
+                                              make_string_identifier(first_topic_name), additional_partitions_count));
+    const auto first_group  = client->create_consumer_group(make_string_identifier(first_stream_name),
+                                                            make_string_identifier(first_topic_name), first_group_name);
+    const auto second_group = client->create_consumer_group(
+        make_string_identifier(first_stream_name), make_string_identifier(second_topic_name), second_group_name);
+    const auto third_group = client->create_consumer_group(make_string_identifier(second_stream_name),
+                                                           make_string_identifier(third_topic_name), third_group_name);
+
+    ASSERT_NO_THROW({ second_client = login_to_server(); });
+    ASSERT_NE(second_client, nullptr);
+    ASSERT_NO_THROW({ third_client = login_to_server(); });
+    ASSERT_NE(third_client, nullptr);
+
+    const auto first_stream_details           = client->get_stream(make_string_identifier(first_stream_name));
+    const auto second_stream_details          = client->get_stream(make_string_identifier(second_stream_name));
+    const std::uint32_t expected_topics_count = first_stream_details.topics_count + second_stream_details.topics_count;
+    std::uint32_t first_topic_partitions      = 0;
+    std::uint32_t second_topic_partitions     = 0;
+    std::uint32_t third_topic_partitions      = 0;
+    for (const auto &topic : first_stream_details.topics) {
+        if (topic.name == first_topic_name) {
+            first_topic_partitions = topic.partitions_count;
+        }
+        if (topic.name == second_topic_name) {
+            second_topic_partitions = topic.partitions_count;
+        }
+    }
+    for (const auto &topic : second_stream_details.topics) {
+        if (topic.name == third_topic_name) {
+            third_topic_partitions = topic.partitions_count;
+        }
+    }
+    const std::uint32_t expected_partitions_count =
+        first_topic_partitions + second_topic_partitions + third_topic_partitions;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 2u);
+        EXPECT_EQ(stats.topics_count, expected_topics_count);
+        EXPECT_EQ(stats.partitions_count, expected_partitions_count);
+        EXPECT_EQ(stats.segments_count, expected_partitions_count);
+        EXPECT_EQ(stats.consumer_groups_count, 3u);
+        EXPECT_EQ(stats.clients_count, empty_stats.clients_count + 2u);
+        EXPECT_EQ(first_group.partitions_count, first_topic_partitions);
+        EXPECT_EQ(second_group.partitions_count, second_topic_partitions);
+        EXPECT_EQ(third_group.partitions_count, third_topic_partitions);
+    });
+
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(second_stream_name)));
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(first_stream_name)));
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(third_client));
+    third_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 0u);
+        EXPECT_EQ(stats.topics_count, 0u);
+        EXPECT_EQ(stats.partitions_count, 0u);
+        EXPECT_EQ(stats.segments_count, 0u);
+        EXPECT_EQ(stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetStatsIsStableAcrossBackToBackCalls) {
+    RecordProperty(
+        "description",
+        "Returns sane invariant fields across back-to-back get_stats calls on an idle authenticated client.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Stats first_stats{};
+    iggy::ffi::Stats second_stats{};
+    ASSERT_NO_THROW({
+        first_stats  = client->get_stats();
+        second_stats = client->get_stats();
+    });
+
+    EXPECT_NE(first_stats.process_id, 0u);
+    EXPECT_NE(second_stats.process_id, 0u);
+    EXPECT_EQ(second_stats.process_id, first_stats.process_id);
+    EXPECT_GT(first_stats.threads_count, 0u);
+    EXPECT_GT(second_stats.threads_count, 0u);
+    EXPECT_GT(first_stats.total_memory, 0u);
+    EXPECT_GT(second_stats.total_memory, 0u);
+    EXPECT_FALSE(static_cast<std::string>(first_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.iggy_server_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.iggy_server_version).empty());
+    EXPECT_EQ(static_cast<std::string>(second_stats.hostname), static_cast<std::string>(first_stats.hostname));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_name), static_cast<std::string>(first_stats.os_name));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_version), static_cast<std::string>(first_stats.os_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.kernel_version),
+              static_cast<std::string>(first_stats.kernel_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.iggy_server_version),
+              static_cast<std::string>(first_stats.iggy_server_version));
+    EXPECT_EQ(second_stats.iggy_server_semver, first_stats.iggy_server_semver);
+    EXPECT_EQ(second_stats.clients_count, first_stats.clients_count);
+    EXPECT_EQ(second_stats.streams_count, first_stats.streams_count);
+    EXPECT_EQ(second_stats.topics_count, first_stats.topics_count);
+    EXPECT_EQ(second_stats.partitions_count, first_stats.partitions_count);
+    EXPECT_EQ(second_stats.consumer_groups_count, first_stats.consumer_groups_count);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_me before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_me(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_me(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add additional validation for get_me after merging join_consumer_group PR.
+TEST(LowLevelE2E_Client, GetMeReturnsCurrentClientDetails) {
+    RecordProperty("description", "Returns the current authenticated client details.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_NO_THROW({
+        const auto me = client->get_me();
+        EXPECT_NE(me.client_id, 0u);
+        EXPECT_NE(me.user_id, std::numeric_limits<std::uint32_t>::max());
+        EXPECT_FALSE(static_cast<std::string>(me.address).empty());
+        EXPECT_EQ(static_cast<std::string>(me.transport), "TCP");
+        EXPECT_EQ(me.consumer_groups_count, 0u);
+        EXPECT_TRUE(me.consumer_groups.empty());
+    });
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeIsStableAcrossBackToBackCalls) {
+    RecordProperty("description", "Returns stable current-client details across back-to-back get_me calls.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({
+        first_me  = client->get_me();
+        second_me = client->get_me();
+    });
+
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_EQ(second_me.client_id, first_me.client_id);
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(second_me.address), static_cast<std::string>(first_me.address));
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), static_cast<std::string>(first_me.transport));
+    EXPECT_EQ(second_me.consumer_groups_count, first_me.consumer_groups_count);
+    EXPECT_EQ(second_me.consumer_groups.size(), first_me.consumer_groups.size());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeReturnsDistinctClientIdsForDifferentSessions) {
+    RecordProperty(
+        "description",
+        "Returns different client ids for separate authenticated sessions while keeping the same user identity.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({
+        first_me  = first_client->get_me();
+        second_me = second_client->get_me();
+    });
+
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_NE(second_me.client_id, 0u);
+    EXPECT_NE(second_me.client_id, first_me.client_id);
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeReturnsValidDetailsAfterReconnect) {
+    RecordProperty("description",
+                   "Returns valid current-client details after reconnecting with a fresh authenticated session.");
+    iggy::ffi::Client *first_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    ASSERT_NO_THROW({ first_me = first_client->get_me(); });
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_NE(first_me.user_id, std::numeric_limits<std::uint32_t>::max());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({ second_me = second_client->get_me(); });
+    EXPECT_NE(second_me.client_id, 0u);
+    EXPECT_NE(second_me.user_id, std::numeric_limits<std::uint32_t>::max());
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), static_cast<std::string>(first_me.transport));
+    EXPECT_FALSE(static_cast<std::string>(second_me.address).empty());
+    EXPECT_EQ(second_me.consumer_groups_count, 0u);
+    EXPECT_TRUE(second_me.consumer_groups.empty());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_client before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_client(1), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_client(1), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientWithWrongClientIdThrows) {
+    RecordProperty("description", "Rejects querying invalid or non-existent client ids.");
+    const std::uint32_t wrong_client_ids[] = {0u, std::numeric_limits<std::uint32_t>::max()};
+    iggy::ffi::Client *client              = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    for (const std::uint32_t wrong_client_id : wrong_client_ids) {
+        SCOPED_TRACE(wrong_client_id);
+        ASSERT_THROW(client->get_client(wrong_client_id), std::exception);
+    }
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientReturnsDetailsForMatchingClientId) {
+    RecordProperty("description", "Returns current client details when querying with the authenticated client id.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::ClientInfoDetails current_client{};
+    iggy::ffi::ClientInfoDetails looked_up_client{};
+    ASSERT_NO_THROW({
+        current_client   = client->get_me();
+        looked_up_client = client->get_client(current_client.client_id);
+    });
+
+    EXPECT_NE(current_client.client_id, 0u);
+    EXPECT_EQ(looked_up_client.client_id, current_client.client_id);
+    EXPECT_EQ(looked_up_client.user_id, current_client.user_id);
+    EXPECT_EQ(static_cast<std::string>(looked_up_client.address), static_cast<std::string>(current_client.address));
+    EXPECT_EQ(static_cast<std::string>(looked_up_client.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(looked_up_client.transport), static_cast<std::string>(current_client.transport));
+    EXPECT_EQ(looked_up_client.consumer_groups_count, current_client.consumer_groups_count);
+    EXPECT_EQ(looked_up_client.consumer_groups.size(), current_client.consumer_groups.size());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientIsStableAcrossBackToBackCalls) {
+    RecordProperty("description", "Returns stable client details across back-to-back get_client calls.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::ClientInfoDetails current_client{};
+    iggy::ffi::ClientInfoDetails first_lookup{};
+    iggy::ffi::ClientInfoDetails second_lookup{};
+    ASSERT_NO_THROW({
+        current_client = client->get_me();
+        first_lookup   = client->get_client(current_client.client_id);
+        second_lookup  = client->get_client(current_client.client_id);
+    });
+
+    EXPECT_NE(current_client.client_id, 0u);
+    EXPECT_EQ(first_lookup.client_id, current_client.client_id);
+    EXPECT_EQ(second_lookup.client_id, first_lookup.client_id);
+    EXPECT_EQ(second_lookup.user_id, first_lookup.user_id);
+    EXPECT_EQ(static_cast<std::string>(second_lookup.address), static_cast<std::string>(first_lookup.address));
+    EXPECT_EQ(static_cast<std::string>(first_lookup.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_lookup.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_lookup.transport), static_cast<std::string>(first_lookup.transport));
+    EXPECT_EQ(second_lookup.consumer_groups_count, first_lookup.consumer_groups_count);
+    EXPECT_EQ(second_lookup.consumer_groups.size(), first_lookup.consumer_groups.size());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_clients before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_clients(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_clients(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsReturnsActiveClientSessions) {
+    RecordProperty("description", "Returns the currently active authenticated client sessions.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails second_me{};
+    rust::Vec<iggy::ffi::ClientInfo> clients;
+    ASSERT_NO_THROW({
+        first_me  = first_client->get_me();
+        second_me = second_client->get_me();
+        clients   = first_client->get_clients();
+    });
+
+    ASSERT_GE(clients.size(), 2u);
+
+    bool found_first  = false;
+    bool found_second = false;
+    for (const auto &client : clients) {
+        EXPECT_NE(client.client_id, 0u);
+        EXPECT_EQ(static_cast<std::string>(client.transport), "TCP");
+
+        if (client.client_id == first_me.client_id) {
+            found_first = true;
+            EXPECT_EQ(client.user_id, first_me.user_id);
+            EXPECT_EQ(static_cast<std::string>(client.address), static_cast<std::string>(first_me.address));
+            EXPECT_EQ(client.consumer_groups_count, first_me.consumer_groups_count);
+        }
+
+        if (client.client_id == second_me.client_id) {
+            found_second = true;
+            EXPECT_EQ(client.user_id, second_me.user_id);
+            EXPECT_EQ(static_cast<std::string>(client.address), static_cast<std::string>(second_me.address));
+            EXPECT_EQ(client.consumer_groups_count, second_me.consumer_groups_count);
+        }
+    }
+
+    EXPECT_TRUE(found_first);
+    EXPECT_TRUE(found_second);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsIsStableAcrossBackToBackCalls) {
+    RecordProperty("description", "Returns stable client lists across back-to-back get_clients calls.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    rust::Vec<iggy::ffi::ClientInfo> first_clients;
+    rust::Vec<iggy::ffi::ClientInfo> second_clients;
+    ASSERT_NO_THROW({
+        first_clients  = first_client->get_clients();
+        second_clients = first_client->get_clients();
+    });
+
+    ASSERT_EQ(second_clients.size(), first_clients.size());
+    for (const auto &first_entry : first_clients) {
+        bool found_match = false;
+        for (const auto &second_entry : second_clients) {
+            if (second_entry.client_id != first_entry.client_id) {
+                continue;
+            }
+
+            found_match = true;
+            EXPECT_EQ(second_entry.user_id, first_entry.user_id);
+            EXPECT_EQ(static_cast<std::string>(second_entry.address), static_cast<std::string>(first_entry.address));
+            EXPECT_EQ(static_cast<std::string>(second_entry.transport),
+                      static_cast<std::string>(first_entry.transport));
+            EXPECT_EQ(second_entry.consumer_groups_count, first_entry.consumer_groups_count);
+            break;
+        }
+        EXPECT_TRUE(found_match);
+    }
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsMatchesGetClientForReturnedIds) {
+    RecordProperty("description", "Returns list entries that agree with get_client for each returned client id.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    rust::Vec<iggy::ffi::ClientInfo> clients;
+    ASSERT_NO_THROW({ clients = first_client->get_clients(); });
+    ASSERT_GE(clients.size(), 2u);
+
+    for (const auto &client : clients) {
+        SCOPED_TRACE(client.client_id);
+        iggy::ffi::ClientInfoDetails details{};
+        ASSERT_NO_THROW({ details = first_client->get_client(client.client_id); });
+
+        EXPECT_EQ(details.client_id, client.client_id);
+        EXPECT_EQ(details.user_id, client.user_id);
+        EXPECT_EQ(static_cast<std::string>(details.address), static_cast<std::string>(client.address));
+        EXPECT_EQ(static_cast<std::string>(details.transport), static_cast<std::string>(client.transport));
+        EXPECT_EQ(details.consumer_groups_count, client.consumer_groups_count);
+    }
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsReflectsAdditionalSession) {
+    RecordProperty("description", "Reflects a newly added authenticated session in subsequent get_clients results.");
+    iggy::ffi::Client *first_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+
+    rust::Vec<iggy::ffi::ClientInfo> clients_before;
+    ASSERT_NO_THROW({ clients_before = first_client->get_clients(); });
+
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails second_me{};
+    rust::Vec<iggy::ffi::ClientInfo> clients_after;
+    ASSERT_NO_THROW({
+        second_me     = second_client->get_me();
+        clients_after = first_client->get_clients();
+    });
+
+    bool found_before = false;
+    for (const auto &client : clients_before) {
+        if (client.client_id == second_me.client_id) {
+            found_before = true;
+            break;
+        }
+    }
+    EXPECT_FALSE(found_before);
+
+    bool found_after = false;
+    for (const auto &client : clients_after) {
+        if (client.client_id != second_me.client_id) {
+            continue;
+        }
+
+        found_after = true;
+        EXPECT_EQ(client.user_id, second_me.user_id);
+        EXPECT_EQ(static_cast<std::string>(client.address), static_cast<std::string>(second_me.address));
+        EXPECT_EQ(static_cast<std::string>(client.transport), "TCP");
+        EXPECT_EQ(client.consumer_groups_count, second_me.consumer_groups_count);
+        break;
+    }
+    EXPECT_TRUE(found_after);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, PingSucceedsForNewConnection) {
+    RecordProperty("description", "Successfully pings the server from a fresh unauthenticated client session.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_NO_THROW(client->ping());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, HeartbeatIntervalReturnsDefaultValueForNewConnection) {
+    RecordProperty("description",
+                   "Returns the default heartbeat interval in microseconds for a fresh unauthenticated client.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    const auto heartbeat_interval = client->heartbeat_interval();
+    EXPECT_EQ(heartbeat_interval, 5'000'000u);

Review Comment:
   Done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3293170604


##########
foreign/cpp/src/lib.rs:
##########
@@ -114,6 +114,72 @@ mod ffi {
         members: Vec<ConsumerGroupMember>,
     }
 
+    struct ConsumerGroupInfo {
+        stream_id: u32,
+        topic_id: u32,
+        group_id: u32,
+    }
+
+    struct ClientInfo {
+        client_id: u32,
+        has_user_id: bool,
+        user_id: u32,
+        address: String,
+        transport: String,
+        consumer_groups_count: u32,
+    }
+
+    struct ClientInfoDetails {
+        client_id: u32,
+        has_user_id: bool,
+        user_id: u32,
+        address: String,
+        transport: String,
+        consumer_groups_count: u32,
+        consumer_groups: Vec<ConsumerGroupInfo>,
+    }
+
+    struct CacheMetricEntry {
+        stream_id: u32,
+        topic_id: u32,
+        partition_id: u32,
+        hits: u64,
+        misses: u64,
+        hit_ratio: f32,
+    }
+
+    struct Stats {
+        process_id: u32,
+        cpu_usage: f32,
+        total_cpu_usage: f32,
+        memory_usage: u64,
+        total_memory: u64,
+        available_memory: u64,
+        run_time: u64,

Review Comment:
   done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3293165279


##########
foreign/cpp/src/identifier.rs:
##########
@@ -67,14 +67,14 @@ impl TryFrom<ffi::Identifier> for RustIdentifier {
 // preserves the C++ ABI used by every test and downstream binding.
 #[allow(clippy::wrong_self_convention)]

Review Comment:
   done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "hubcio (via GitHub)" <gi...@apache.org>.
hubcio commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3274399858


##########
foreign/cpp/include/iggy.hpp:
##########
@@ -26,46 +26,128 @@
 
 namespace iggy {
 
+/// Exception raised by the C++ client when an operation fails.
 class IggyException : public std::runtime_error {
   public:
     explicit IggyException(const char *message) : std::runtime_error(message) {}
     explicit IggyException(const std::string &message) : std::runtime_error(message) {}
 };
 
-class CompressionAlgorithm final {
+namespace detail {
+
+/// Internal helper used by string-backed option types in this header.
+template <typename Tag>
+class StringTag {
+  protected:
+    explicit StringTag(std::string value) : value_(std::move(value)) {}
+
+    std::string_view value() const { return value_; }
+
+  private:
+    std::string value_;
+};
+
+}  // namespace detail
+
+/// Compression setting for `Client::create_topic(...)`.
+///
+/// Use this type to choose whether messages in a topic are stored as-is or compressed with gzip.
+///
+/// Internally, this value is passed across the Rust FFI as a string.
+/// Values outside the supported set are rejected by the Rust client.
+/// Some Rust-side aliases may also be accepted for compatibility.

Review Comment:
   this `"Some Rust-side aliases may also be accepted for compatibility."` line is repeated on `SnapshotCompression` at line 78 and `SnapshotType` at line 107. only `MaxTopicSize::from_str` accepts genuine token aliases (`"0"` ≡ `"server_default"`). the other three only do `s.to_lowercase()` then exact-match (`core/common/src/types/compression/compression_algorithm.rs:45-51`, `core/common/src/types/snapshot/mod.rs:129-141, 171-181`). drop the alias claim from the three inaccurate docs, keep on `MaxTopicSize` only.



##########
foreign/cpp/include/iggy.hpp:
##########
@@ -26,46 +26,128 @@
 
 namespace iggy {
 
+/// Exception raised by the C++ client when an operation fails.
 class IggyException : public std::runtime_error {
   public:
     explicit IggyException(const char *message) : std::runtime_error(message) {}
     explicit IggyException(const std::string &message) : std::runtime_error(message) {}
 };
 
-class CompressionAlgorithm final {
+namespace detail {
+
+/// Internal helper used by string-backed option types in this header.
+template <typename Tag>
+class StringTag {
+  protected:
+    explicit StringTag(std::string value) : value_(std::move(value)) {}
+
+    std::string_view value() const { return value_; }
+
+  private:
+    std::string value_;
+};
+
+}  // namespace detail
+
+/// Compression setting for `Client::create_topic(...)`.
+///
+/// Use this type to choose whether messages in a topic are stored as-is or compressed with gzip.
+///
+/// Internally, this value is passed across the Rust FFI as a string.
+/// Values outside the supported set are rejected by the Rust client.
+/// Some Rust-side aliases may also be accepted for compatibility.
+class CompressionAlgorithm final : private detail::StringTag<CompressionAlgorithm> {
   public:
+    /// Store messages without compression.
     static CompressionAlgorithm none() { return CompressionAlgorithm("none"); }
+    /// Compress messages with gzip.
     static CompressionAlgorithm gzip() { return CompressionAlgorithm("gzip"); }
 
-    std::string_view compression_algorithm_value() const { return algorithm_; }
+    std::string_view compression_algorithm_value() const { return value(); }
 
   private:
-    explicit CompressionAlgorithm(std::string algorithm) : algorithm_(std::move(algorithm)) {}
-
-    std::string algorithm_;
+    explicit CompressionAlgorithm(std::string algorithm)
+        : detail::StringTag<CompressionAlgorithm>(std::move(algorithm)) {}
 };
 
-class Expiry final {
+/// Compression setting for `Client::snapshot(...)`.
+///
+/// Use this type to choose how snapshot data is compressed in the generated archive.
+///
+/// Internally, this value is passed across the Rust FFI as a string.
+/// Values outside the supported set are rejected by the Rust client.
+/// Some Rust-side aliases may also be accepted for compatibility.
+class SnapshotCompression final : private detail::StringTag<SnapshotCompression> {
   public:
-    static Expiry server_default() { return Expiry("server_default", 0); }
-    static Expiry never_expire() { return Expiry("never_expire", std::numeric_limits<std::uint64_t>::max()); }
-    static Expiry duration(std::uint64_t micros) { return Expiry("duration", micros); }
-
-    std::string_view expiry_kind() const { return expiry_kind_; }
-    std::uint64_t expiry_value() const { return expiry_value_; }
+    /// Store snapshot files without compression.
+    static SnapshotCompression stored() { return SnapshotCompression("stored"); }
+    /// Use standard deflate compression.
+    static SnapshotCompression deflated() { return SnapshotCompression("deflated"); }
+    /// Use bzip2 for a higher compression ratio at the cost of slower processing.
+    static SnapshotCompression bzip2() { return SnapshotCompression("bzip2"); }
+    /// Use Zstandard for fast compression and decompression.
+    static SnapshotCompression zstd() { return SnapshotCompression("zstd"); }
+    /// Use LZMA for high compression, especially for larger files.
+    static SnapshotCompression lzma() { return SnapshotCompression("lzma"); }
+    /// Use XZ, which is similar to LZMA and often faster to decompress.
+    static SnapshotCompression xz() { return SnapshotCompression("xz"); }
+
+    std::string_view snapshot_compression_value() const { return value(); }
 
   private:
-    explicit Expiry(std::string expiry_kind, std::uint64_t expiry_value)
-        : expiry_kind_(std::move(expiry_kind)), expiry_value_(expiry_value) {}
+    explicit SnapshotCompression(std::string snapshot_compression)
+        : detail::StringTag<SnapshotCompression>(std::move(snapshot_compression)) {}
+};
 
-    std::string expiry_kind_;
-    std::uint64_t expiry_value_;
+/// Snapshot selector for `Client::snapshot(...)`.
+///
+/// Use this type to choose which snapshot data the server should include.
+///
+/// Internally, each selected value is passed across the Rust FFI as a string.
+/// Values outside the supported set are rejected by the Rust client.
+/// Some Rust-side aliases may also be accepted for compatibility.
+class SnapshotType final : private detail::StringTag<SnapshotType> {

Review Comment:
   rust calls this `SystemSnapshotType` (`core/common/src/types/snapshot/mod.rs:36`) and c# preserves the prefix in `foreign/csharp/Iggy_SDK/Enums/SystemSnapshotType.cs`. c++ binding drops `System`. rename to `SystemSnapshotType` while pre-release to stop the divergence.



##########
foreign/cpp/src/client.rs:
##########
@@ -604,6 +745,50 @@ impl Client {
             Ok(())
         })
     }
+
+    pub fn heartbeat_interval(&self) -> u64 {
+        // The upstream client exposes this config-derived value via an async API,
+        // so the synchronous C++ wrapper reads it by blocking on the runtime.
+        RUNTIME.block_on(async { self.inner.heartbeat_interval().await.as_micros() })
+    }
+
+    pub fn snapshot(

Review Comment:
   empty `snapshot_types` vec → server loops zero times in `core/server/src/shard/system/snapshot/mod.rs:71` and returns an empty zip with `Ok(())`. silent no-op. duplicate types collide on the `{snapshot_type}.txt` filename at line 80, so the second entry quietly overwrites the first. reject empty vec at the ffi boundary and dedup before passing to the sdk.



##########
foreign/cpp/src/client.rs:
##########
@@ -501,14 +590,12 @@ impl Client {
                 .await
                 .map_err(|error| {
                     format!(
-                        "Could not get consumer group '{}' for topic '{}' on stream '{}': {error}",
-                        rust_group_id, rust_topic_id, rust_stream_id
+                        "Could not get consumer group '{rust_group_id}' for topic '{rust_topic_id}' on stream '{rust_stream_id}': {error}"

Review Comment:
   follow-up on the earlier `clippy::uninlined_format_args` thread: the sweep landed here and at line 627, but missed two callers - `join_consumer_group` at lines 656-657 and `leave_consumer_group` at 741-742 still use the old positional `{}` style with separate args. unify to named captures.



##########
foreign/cpp/src/client.rs:
##########
@@ -21,15 +21,119 @@ use iggy::prelude::{
     ConsumerGroupClient, Identifier as RustIdentifier, IggyClient as RustIggyClient,
     IggyClientBuilder as RustIggyClientBuilder, IggyError, IggyExpiry as RustIggyExpiry,
     IggyMessage, IggyTimestamp, MaxTopicSize as RustMaxTopicSize, MessageClient, PartitionClient,
-    Partitioning, PollingStrategy, StreamClient, TopicClient, UserClient,
+    Partitioning, PollingStrategy, SnapshotCompression as RustSnapshotCompression, StreamClient,
+    SystemClient as RustSystemClient, SystemSnapshotType as RustSystemSnapshotType, TopicClient,
+    UserClient,
 };
+use iggy_common::{
+    CacheMetrics as RustCacheMetrics, CacheMetricsKey as RustCacheMetricsKey,
+    ClientInfo as RustClientInfo, ClientInfoDetails as RustClientInfoDetails, Stats as RustStats,
+};
+use std::convert::TryFrom;
 use std::str::FromStr;
 use std::sync::Arc;
 
 /// Sentinel value passed from C++ to mean "no partition specified" — the server picks the
 /// partition based on the consumer/strategy. Cxx FFI does not support `Option<u32>`, so we
 /// reserve `u32::MAX` as the sentinel for `partition_id`.
 const ANY_PARTITION_ID: u32 = u32::MAX;
+impl From<RustClientInfo> for ffi::ClientInfo {
+    fn from(client: RustClientInfo) -> Self {
+        let has_user_id = client.user_id.is_some();
+        ffi::ClientInfo {
+            client_id: client.client_id,
+            has_user_id,
+            user_id: client.user_id.unwrap_or(0),
+            address: client.address,
+            transport: client.transport,
+            consumer_groups_count: client.consumer_groups_count,
+        }
+    }
+}
+
+impl From<RustClientInfoDetails> for ffi::ClientInfoDetails {
+    fn from(client: RustClientInfoDetails) -> Self {
+        let has_user_id = client.user_id.is_some();
+        ffi::ClientInfoDetails {
+            client_id: client.client_id,
+            has_user_id,
+            user_id: client.user_id.unwrap_or(0),
+            address: client.address,
+            transport: client.transport,
+            consumer_groups_count: client.consumer_groups_count,
+            consumer_groups: client
+                .consumer_groups
+                .into_iter()
+                .map(ffi::ConsumerGroupInfo::from)
+                .collect(),
+        }
+    }
+}
+
+impl TryFrom<Option<RustClientInfoDetails>> for ffi::ClientInfoDetails {
+    type Error = String;
+
+    fn try_from(client: Option<RustClientInfoDetails>) -> Result<Self, Self::Error> {
+        match client {
+            Some(client) => Ok(ffi::ClientInfoDetails::from(client)),
+            None => Err("client not found".to_string()),
+        }
+    }
+}
+
+impl From<(RustCacheMetricsKey, RustCacheMetrics)> for ffi::CacheMetricEntry {
+    fn from((key, metrics): (RustCacheMetricsKey, RustCacheMetrics)) -> Self {
+        ffi::CacheMetricEntry {
+            stream_id: key.stream_id,
+            topic_id: key.topic_id,
+            partition_id: key.partition_id,
+            hits: metrics.hits,
+            misses: metrics.misses,
+            hit_ratio: metrics.hit_ratio,
+        }
+    }
+}
+
+impl From<RustStats> for ffi::Stats {
+    fn from(stats: RustStats) -> Self {
+        let has_server_semver = stats.iggy_server_semver.is_some();
+        ffi::Stats {
+            process_id: stats.process_id,
+            cpu_usage: stats.cpu_usage,
+            total_cpu_usage: stats.total_cpu_usage,
+            memory_usage: stats.memory_usage.as_bytes_u64(),
+            total_memory: stats.total_memory.as_bytes_u64(),
+            available_memory: stats.available_memory.as_bytes_u64(),
+            run_time: stats.run_time.as_micros(),
+            start_time: stats.start_time.as_micros(),
+            read_bytes: stats.read_bytes.as_bytes_u64(),
+            written_bytes: stats.written_bytes.as_bytes_u64(),
+            messages_size_bytes: stats.messages_size_bytes.as_bytes_u64(),
+            streams_count: stats.streams_count,
+            topics_count: stats.topics_count,
+            partitions_count: stats.partitions_count,
+            segments_count: stats.segments_count,
+            messages_count: stats.messages_count,
+            clients_count: stats.clients_count,
+            consumer_groups_count: stats.consumer_groups_count,
+            hostname: stats.hostname,
+            os_name: stats.os_name,
+            os_version: stats.os_version,
+            kernel_version: stats.kernel_version,
+            iggy_server_version: stats.iggy_server_version,
+            has_server_semver,
+            iggy_server_semver: stats.iggy_server_semver.unwrap_or(0),

Review Comment:
   same `unwrap_or(0)` shape as the `user_id` follow-up. wire decoder at `core/binary_protocol/src/responses/system/get_stats.rs:204` forbids `Some(0)` from a conforming server so the collision is unreachable today, but the pattern is fragile - if anyone ever changes the wire format to allow `Some(0)`, this silently degrades to the same shape as `None`. doc the invariant on the bridge struct, or align to a sentinel that cannot collide.



##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -150,3 +154,702 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbmR): add a test to create some streams, topics, partitions, and segments, send messages, and create
+// consumer groups and verify it.
+TEST(LowLevelE2E_Client, GetStatsReturnsServerStats) {

Review Comment:
   `tests/common/test_helpers.hpp:18` already has a `TODO(slbotbm): create fixture for setup/teardown` covering the broader cleanup story, but this specific test has two extra fragilities not implicit in that todo: (1) fixed stream/topic/group names (`cpp-get-stats-stream-1` etc) - any failing `ASSERT_NO_THROW` before the inline cleanup at lines 266-272 leaks streams and the next run collides on `create_stream`; (2) `clients_count == empty + 2u` (line 260) compares deltas against `empty_stats` taken seconds earlier on a shared server - any other client connecting in between invalidates the assertion. once the fixture lands, randomize names and switch the count assertions to `EXPECT_GE` so this test stops being order-dependent. add a `TODO` here if the fix isn't landing in this pr.



##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -150,3 +154,702 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbmR): add a test to create some streams, topics, partitions, and segments, send messages, and create
+// consumer groups and verify it.
+TEST(LowLevelE2E_Client, GetStatsReturnsServerStats) {
+    RecordProperty("description",
+                   "Returns empty resource counts first, then reflects aggregated streams, topics, partitions, "
+                   "consumer groups, and clients.");
+    const std::string first_stream_name                 = "cpp-get-stats-stream-1";
+    const std::string second_stream_name                = "cpp-get-stats-stream-2";
+    const std::string first_topic_name                  = "cpp-get-stats-topic-1";
+    const std::string second_topic_name                 = "cpp-get-stats-topic-2";
+    const std::string third_topic_name                  = "cpp-get-stats-topic-3";
+    const std::string first_group_name                  = "cpp-get-stats-group-1";
+    const std::string second_group_name                 = "cpp-get-stats-group-2";
+    const std::string third_group_name                  = "cpp-get-stats-group-3";
+    constexpr std::uint32_t additional_partitions_count = 2;
+    iggy::ffi::Client *client                           = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Client *second_client = nullptr;
+    iggy::ffi::Client *third_client  = nullptr;
+
+    iggy::ffi::Stats empty_stats{};
+    ASSERT_NO_THROW({
+        empty_stats = client->get_stats();
+        EXPECT_NE(empty_stats.process_id, 0u);
+        EXPECT_GT(empty_stats.threads_count, 0u);
+        EXPECT_GT(empty_stats.total_memory, 0u);
+        EXPECT_LE(empty_stats.available_memory, empty_stats.total_memory);
+        EXPECT_GE(empty_stats.total_disk_space, empty_stats.free_disk_space);
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.hostname).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_name).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.kernel_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.iggy_server_version).empty());
+        EXPECT_TRUE(!empty_stats.has_server_semver || empty_stats.iggy_server_semver > 0u);

Review Comment:
   `!has_server_semver || iggy_server_semver > 0u` is unfalsifiable. wire decoder at `core/binary_protocol/src/responses/system/get_stats.rs:204` sets `Some(raw)` iff `raw != 0`, so `has_server_semver=true` already implies `iggy_server_semver > 0`. use `EXPECT_EQ(empty_stats.has_server_semver, empty_stats.iggy_server_semver != 0u)` or drop the assertion.



##########
foreign/cpp/src/lib.rs:
##########
@@ -114,6 +114,72 @@ mod ffi {
         members: Vec<ConsumerGroupMember>,
     }
 
+    struct ConsumerGroupInfo {
+        stream_id: u32,
+        topic_id: u32,
+        group_id: u32,
+    }
+
+    struct ClientInfo {
+        client_id: u32,
+        has_user_id: bool,
+        user_id: u32,
+        address: String,
+        transport: String,
+        consumer_groups_count: u32,
+    }
+
+    struct ClientInfoDetails {
+        client_id: u32,
+        has_user_id: bool,
+        user_id: u32,
+        address: String,
+        transport: String,
+        consumer_groups_count: u32,
+        consumer_groups: Vec<ConsumerGroupInfo>,
+    }
+
+    struct CacheMetricEntry {
+        stream_id: u32,
+        topic_id: u32,
+        partition_id: u32,
+        hits: u64,
+        misses: u64,
+        hit_ratio: f32,
+    }
+
+    struct Stats {
+        process_id: u32,
+        cpu_usage: f32,
+        total_cpu_usage: f32,
+        memory_usage: u64,
+        total_memory: u64,
+        available_memory: u64,
+        run_time: u64,

Review Comment:
   `run_time` and `start_time` are both bare `u64` but carry different semantics - `client.rs:107-108` fills them as `stats.run_time.as_micros()` (`IggyDuration`, micros since boot) and `stats.start_time.as_micros()` (`IggyTimestamp`, unix-epoch micros). consumers cannot tell them apart without reading the rust source. rename to `run_time_micros` and `start_time_epoch_micros`, or doc-comment the bridge struct.



##########
foreign/cpp/src/client.rs:
##########
@@ -21,15 +21,119 @@ use iggy::prelude::{
     ConsumerGroupClient, Identifier as RustIdentifier, IggyClient as RustIggyClient,
     IggyClientBuilder as RustIggyClientBuilder, IggyError, IggyExpiry as RustIggyExpiry,
     IggyMessage, IggyTimestamp, MaxTopicSize as RustMaxTopicSize, MessageClient, PartitionClient,
-    Partitioning, PollingStrategy, StreamClient, TopicClient, UserClient,
+    Partitioning, PollingStrategy, SnapshotCompression as RustSnapshotCompression, StreamClient,
+    SystemClient as RustSystemClient, SystemSnapshotType as RustSystemSnapshotType, TopicClient,
+    UserClient,
 };
+use iggy_common::{
+    CacheMetrics as RustCacheMetrics, CacheMetricsKey as RustCacheMetricsKey,
+    ClientInfo as RustClientInfo, ClientInfoDetails as RustClientInfoDetails, Stats as RustStats,
+};
+use std::convert::TryFrom;
 use std::str::FromStr;
 use std::sync::Arc;
 
 /// Sentinel value passed from C++ to mean "no partition specified" — the server picks the
 /// partition based on the consumer/strategy. Cxx FFI does not support `Option<u32>`, so we
 /// reserve `u32::MAX` as the sentinel for `partition_id`.
 const ANY_PARTITION_ID: u32 = u32::MAX;
+impl From<RustClientInfo> for ffi::ClientInfo {
+    fn from(client: RustClientInfo) -> Self {
+        let has_user_id = client.user_id.is_some();
+        ffi::ClientInfo {
+            client_id: client.client_id,
+            has_user_id,
+            user_id: client.user_id.unwrap_or(0),

Review Comment:
   follow-up on the earlier `unwrap_or(u32::MAX)` thread: switching to `unwrap_or(0)` here and at line 60 trades one collision for another - root user has id 0 (`core/common/src/http/users/defaults.rs:26`, applied at `core/server/src/streaming/users/user.rs:96`), so `(has_user_id=true, user_id=0)` for a root login is indistinguishable from `(has_user_id=false, user_id=0)` for an unauthenticated session whenever a consumer reads `user_id` without first checking the bool. wire encoding already uses `u32::MAX` as the none sentinel (`core/server/src/binary/handlers/system/get_me_handler.rs:64`, decoder at `core/common/src/wire_conversions.rs:258-261`). either doc the `has_user_id` contract on the bridge structs (`lib.rs:123-130, 132-140`), or align ffi to the wire sentinel with `unwrap_or(u32::MAX)`.



##########
foreign/cpp/src/identifier.rs:
##########
@@ -67,14 +67,14 @@ impl TryFrom<ffi::Identifier> for RustIdentifier {
 // preserves the C++ ABI used by every test and downstream binding.
 #[allow(clippy::wrong_self_convention)]

Review Comment:
   follow-up on the earlier rename thread: `from_string` → `set_string` and `from_numeric` → `set_numeric` landed but the `#[allow(clippy::wrong_self_convention)]` here is now dead (`set_*` doesn't trigger that lint) and the comment above about "keeping the names preserves the C++ ABI used by every test and downstream binding" directly contradicts the rename in this same pr. delete both.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3161634878


##########
foreign/cpp/src/client.rs:
##########
@@ -378,6 +479,98 @@ impl Client {
             Ok(())
         })
     }
+
+    pub fn get_stats(&self) -> Result<ffi::Stats, String> {
+        RUNTIME.block_on(async {
+            let stats = self
+                .inner
+                .get_stats()
+                .await
+                .map_err(|error| format!("Could not get stats: {error}"))?;
+            Ok(ffi::Stats::from(stats))
+        })
+    }
+
+    pub fn get_me(&self) -> Result<ffi::ClientInfoDetails, String> {
+        RUNTIME.block_on(async {
+            let client = self
+                .inner
+                .get_me()
+                .await
+                .map_err(|error| format!("Could not get current client info: {error}"))?;
+            Ok(ffi::ClientInfoDetails::from(client))
+        })
+    }
+
+    pub fn get_client(&self, client_id: u32) -> Result<ffi::ClientInfoDetails, String> {
+        RUNTIME.block_on(async {
+            let client = self
+                .inner
+                .get_client(client_id)
+                .await
+                .map_err(|error| format!("Could not get client '{client_id}': {error}"))?;
+            ffi::ClientInfoDetails::try_from(client)
+                .map_err(|error| format!("Could not get client '{client_id}': {error}"))
+        })
+    }
+
+    pub fn get_clients(&self) -> Result<Vec<ffi::ClientInfo>, String> {
+        RUNTIME.block_on(async {
+            let clients = self
+                .inner
+                .get_clients()
+                .await
+                .map_err(|error| format!("Could not get clients: {error}"))?;
+            Ok(clients.into_iter().map(ffi::ClientInfo::from).collect())
+        })
+    }
+
+    pub fn ping(&self) -> Result<(), String> {
+        RUNTIME.block_on(async {
+            self.inner
+                .ping()
+                .await
+                .map_err(|error| format!("Could not ping server: {error}"))?;
+            Ok(())
+        })
+    }
+
+    pub fn heartbeat_interval(&self) -> u64 {
+        RUNTIME.block_on(async { self.inner.heartbeat_interval().await.as_micros() })
+    }
+
+    pub fn snapshot(
+        &self,
+        snapshot_compression: String,
+        snapshot_types: Vec<String>,
+    ) -> Result<Vec<u8>, String> {
+        let rust_compression = match snapshot_compression.trim() {
+            "" => RustSnapshotCompression::default(),
+            value => RustSnapshotCompression::from_str(value).map_err(|error| {
+                format!("Could not capture snapshot: invalid compression '{value}': {error}")
+            })?,
+        };
+        let rust_snapshot_types = snapshot_types
+            .into_iter()
+            .map(|snapshot_type| {
+                RustSystemSnapshotType::from_str(&snapshot_type).map_err(|error| {
+                    format!(
+                        "Could not capture snapshot: invalid snapshot type '{}': {}",
+                        snapshot_type, error
+                    )
+                })
+            })
+            .collect::<Result<Vec<_>, _>>()?;
+
+        RUNTIME.block_on(async {
+            let snapshot = self
+                .inner
+                .snapshot(rust_compression, rust_snapshot_types)
+                .await
+                .map_err(|error| format!("Could not capture snapshot: {error}"))?;
+            Ok(snapshot.0)

Review Comment:
   done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3161471467


##########
foreign/cpp/src/client.rs:
##########
@@ -20,11 +20,112 @@ use iggy::prelude::{
     Client as IggyConnectionClient, CompressionAlgorithm as RustCompressionAlgorithm,
     ConsumerGroupClient, Identifier as RustIdentifier, IggyClient as RustIggyClient,
     IggyClientBuilder as RustIggyClientBuilder, IggyError, IggyExpiry as RustIggyExpiry,
-    MaxTopicSize as RustMaxTopicSize, PartitionClient, StreamClient, TopicClient, UserClient,
+    MaxTopicSize as RustMaxTopicSize, PartitionClient,
+    SnapshotCompression as RustSnapshotCompression, StreamClient, SystemClient as RustSystemClient,
+    SystemSnapshotType as RustSystemSnapshotType, TopicClient, UserClient,
 };
+use iggy_common::{
+    CacheMetrics as RustCacheMetrics, CacheMetricsKey as RustCacheMetricsKey,
+    ClientInfo as RustClientInfo, ClientInfoDetails as RustClientInfoDetails, Stats as RustStats,
+};
+use std::convert::TryFrom;
 use std::str::FromStr;
 use std::sync::Arc;
 
+impl From<RustClientInfo> for ffi::ClientInfo {
+    fn from(client: RustClientInfo) -> Self {
+        ffi::ClientInfo {
+            client_id: client.client_id,
+            // TODO(slbotbm): In high-level client, this should be converted to None.
+            user_id: client.user_id.unwrap_or(u32::MAX),

Review Comment:
   added has_user_id



##########
foreign/cpp/src/client.rs:
##########
@@ -20,11 +20,112 @@ use iggy::prelude::{
     Client as IggyConnectionClient, CompressionAlgorithm as RustCompressionAlgorithm,
     ConsumerGroupClient, Identifier as RustIdentifier, IggyClient as RustIggyClient,
     IggyClientBuilder as RustIggyClientBuilder, IggyError, IggyExpiry as RustIggyExpiry,
-    MaxTopicSize as RustMaxTopicSize, PartitionClient, StreamClient, TopicClient, UserClient,
+    MaxTopicSize as RustMaxTopicSize, PartitionClient,
+    SnapshotCompression as RustSnapshotCompression, StreamClient, SystemClient as RustSystemClient,
+    SystemSnapshotType as RustSystemSnapshotType, TopicClient, UserClient,
 };
+use iggy_common::{
+    CacheMetrics as RustCacheMetrics, CacheMetricsKey as RustCacheMetricsKey,
+    ClientInfo as RustClientInfo, ClientInfoDetails as RustClientInfoDetails, Stats as RustStats,
+};
+use std::convert::TryFrom;
 use std::str::FromStr;
 use std::sync::Arc;
 
+impl From<RustClientInfo> for ffi::ClientInfo {
+    fn from(client: RustClientInfo) -> Self {
+        ffi::ClientInfo {
+            client_id: client.client_id,
+            // TODO(slbotbm): In high-level client, this should be converted to None.
+            user_id: client.user_id.unwrap_or(u32::MAX),
+            address: client.address,
+            transport: client.transport,
+            consumer_groups_count: client.consumer_groups_count,
+        }
+    }
+}
+
+impl From<RustClientInfoDetails> for ffi::ClientInfoDetails {
+    fn from(client: RustClientInfoDetails) -> Self {
+        ffi::ClientInfoDetails {
+            client_id: client.client_id,
+            // TODO(slbotbm): In high-level client, this should be converted to None.
+            user_id: client.user_id.unwrap_or(u32::MAX),

Review Comment:
   fixed



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3252463677


##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -149,3 +153,671 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add a test to create some streams, topics, partitions, and segments, send messages, and create
+// consumer groups and verify it.
+TEST(LowLevelE2E_Client, GetStatsReturnsServerStats) {
+    RecordProperty("description",
+                   "Returns empty resource counts first, then reflects aggregated streams, topics, partitions, "
+                   "consumer groups, and clients.");
+    const std::string first_stream_name                 = "cpp-get-stats-stream-1";
+    const std::string second_stream_name                = "cpp-get-stats-stream-2";
+    const std::string first_topic_name                  = "cpp-get-stats-topic-1";
+    const std::string second_topic_name                 = "cpp-get-stats-topic-2";
+    const std::string third_topic_name                  = "cpp-get-stats-topic-3";
+    const std::string first_group_name                  = "cpp-get-stats-group-1";
+    const std::string second_group_name                 = "cpp-get-stats-group-2";
+    const std::string third_group_name                  = "cpp-get-stats-group-3";
+    constexpr std::uint32_t additional_partitions_count = 2;
+    iggy::ffi::Client *client                           = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Client *second_client = nullptr;
+    iggy::ffi::Client *third_client  = nullptr;
+
+    iggy::ffi::Stats empty_stats{};
+    ASSERT_NO_THROW({
+        empty_stats = client->get_stats();
+        EXPECT_NE(empty_stats.process_id, 0u);
+        EXPECT_GT(empty_stats.threads_count, 0u);
+        EXPECT_GT(empty_stats.total_memory, 0u);
+        EXPECT_GE(empty_stats.available_memory, 0u);
+        EXPECT_GE(empty_stats.total_disk_space, empty_stats.free_disk_space);
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.hostname).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_name).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.kernel_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.iggy_server_version).empty());
+        EXPECT_TRUE(empty_stats.iggy_server_semver == std::numeric_limits<std::uint32_t>::max() ||
+                    empty_stats.iggy_server_semver > 0u);
+        EXPECT_GE(empty_stats.cache_metrics.size(), 0u);
+        EXPECT_EQ(empty_stats.streams_count, 0u);

Review Comment:
   now mid-test checks happen with empty stats + delta, and post-cleanup, stats are compared to original baseline.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3293156945


##########
foreign/cpp/include/iggy.hpp:
##########
@@ -26,46 +26,128 @@
 
 namespace iggy {
 
+/// Exception raised by the C++ client when an operation fails.
 class IggyException : public std::runtime_error {
   public:
     explicit IggyException(const char *message) : std::runtime_error(message) {}
     explicit IggyException(const std::string &message) : std::runtime_error(message) {}
 };
 
-class CompressionAlgorithm final {
+namespace detail {
+
+/// Internal helper used by string-backed option types in this header.
+template <typename Tag>
+class StringTag {
+  protected:
+    explicit StringTag(std::string value) : value_(std::move(value)) {}
+
+    std::string_view value() const { return value_; }
+
+  private:
+    std::string value_;
+};
+
+}  // namespace detail
+
+/// Compression setting for `Client::create_topic(...)`.
+///
+/// Use this type to choose whether messages in a topic are stored as-is or compressed with gzip.
+///
+/// Internally, this value is passed across the Rust FFI as a string.
+/// Values outside the supported set are rejected by the Rust client.
+/// Some Rust-side aliases may also be accepted for compatibility.

Review Comment:
   removed wrong documentation and made max topic size documentation more explicit.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3293190890


##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -150,3 +154,702 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbmR): add a test to create some streams, topics, partitions, and segments, send messages, and create
+// consumer groups and verify it.
+TEST(LowLevelE2E_Client, GetStatsReturnsServerStats) {

Review Comment:
   Adding a TODO



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3293170604


##########
foreign/cpp/src/lib.rs:
##########
@@ -114,6 +114,72 @@ mod ffi {
         members: Vec<ConsumerGroupMember>,
     }
 
+    struct ConsumerGroupInfo {
+        stream_id: u32,
+        topic_id: u32,
+        group_id: u32,
+    }
+
+    struct ClientInfo {
+        client_id: u32,
+        has_user_id: bool,
+        user_id: u32,
+        address: String,
+        transport: String,
+        consumer_groups_count: u32,
+    }
+
+    struct ClientInfoDetails {
+        client_id: u32,
+        has_user_id: bool,
+        user_id: u32,
+        address: String,
+        transport: String,
+        consumer_groups_count: u32,
+        consumer_groups: Vec<ConsumerGroupInfo>,
+    }
+
+    struct CacheMetricEntry {
+        stream_id: u32,
+        topic_id: u32,
+        partition_id: u32,
+        hits: u64,
+        misses: u64,
+        hit_ratio: f32,
+    }
+
+    struct Stats {
+        process_id: u32,
+        cpu_usage: f32,
+        total_cpu_usage: f32,
+        memory_usage: u64,
+        total_memory: u64,
+        available_memory: u64,
+        run_time: u64,

Review Comment:
   renamed done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3293137566


##########
foreign/cpp/include/iggy.hpp:
##########
@@ -26,46 +26,128 @@
 
 namespace iggy {
 
+/// Exception raised by the C++ client when an operation fails.
 class IggyException : public std::runtime_error {
   public:
     explicit IggyException(const char *message) : std::runtime_error(message) {}
     explicit IggyException(const std::string &message) : std::runtime_error(message) {}
 };
 
-class CompressionAlgorithm final {
+namespace detail {
+
+/// Internal helper used by string-backed option types in this header.
+template <typename Tag>
+class StringTag {
+  protected:
+    explicit StringTag(std::string value) : value_(std::move(value)) {}
+
+    std::string_view value() const { return value_; }
+
+  private:
+    std::string value_;
+};
+
+}  // namespace detail
+
+/// Compression setting for `Client::create_topic(...)`.
+///
+/// Use this type to choose whether messages in a topic are stored as-is or compressed with gzip.
+///
+/// Internally, this value is passed across the Rust FFI as a string.
+/// Values outside the supported set are rejected by the Rust client.
+/// Some Rust-side aliases may also be accepted for compatibility.
+class CompressionAlgorithm final : private detail::StringTag<CompressionAlgorithm> {
   public:
+    /// Store messages without compression.
     static CompressionAlgorithm none() { return CompressionAlgorithm("none"); }
+    /// Compress messages with gzip.
     static CompressionAlgorithm gzip() { return CompressionAlgorithm("gzip"); }
 
-    std::string_view compression_algorithm_value() const { return algorithm_; }
+    std::string_view compression_algorithm_value() const { return value(); }
 
   private:
-    explicit CompressionAlgorithm(std::string algorithm) : algorithm_(std::move(algorithm)) {}
-
-    std::string algorithm_;
+    explicit CompressionAlgorithm(std::string algorithm)
+        : detail::StringTag<CompressionAlgorithm>(std::move(algorithm)) {}
 };
 
-class Expiry final {
+/// Compression setting for `Client::snapshot(...)`.
+///
+/// Use this type to choose how snapshot data is compressed in the generated archive.
+///
+/// Internally, this value is passed across the Rust FFI as a string.
+/// Values outside the supported set are rejected by the Rust client.
+/// Some Rust-side aliases may also be accepted for compatibility.
+class SnapshotCompression final : private detail::StringTag<SnapshotCompression> {
   public:
-    static Expiry server_default() { return Expiry("server_default", 0); }
-    static Expiry never_expire() { return Expiry("never_expire", std::numeric_limits<std::uint64_t>::max()); }
-    static Expiry duration(std::uint64_t micros) { return Expiry("duration", micros); }
-
-    std::string_view expiry_kind() const { return expiry_kind_; }
-    std::uint64_t expiry_value() const { return expiry_value_; }
+    /// Store snapshot files without compression.
+    static SnapshotCompression stored() { return SnapshotCompression("stored"); }
+    /// Use standard deflate compression.
+    static SnapshotCompression deflated() { return SnapshotCompression("deflated"); }
+    /// Use bzip2 for a higher compression ratio at the cost of slower processing.
+    static SnapshotCompression bzip2() { return SnapshotCompression("bzip2"); }
+    /// Use Zstandard for fast compression and decompression.
+    static SnapshotCompression zstd() { return SnapshotCompression("zstd"); }
+    /// Use LZMA for high compression, especially for larger files.
+    static SnapshotCompression lzma() { return SnapshotCompression("lzma"); }
+    /// Use XZ, which is similar to LZMA and often faster to decompress.
+    static SnapshotCompression xz() { return SnapshotCompression("xz"); }
+
+    std::string_view snapshot_compression_value() const { return value(); }
 
   private:
-    explicit Expiry(std::string expiry_kind, std::uint64_t expiry_value)
-        : expiry_kind_(std::move(expiry_kind)), expiry_value_(expiry_value) {}
+    explicit SnapshotCompression(std::string snapshot_compression)
+        : detail::StringTag<SnapshotCompression>(std::move(snapshot_compression)) {}
+};
 
-    std::string expiry_kind_;
-    std::uint64_t expiry_value_;
+/// Snapshot selector for `Client::snapshot(...)`.
+///
+/// Use this type to choose which snapshot data the server should include.
+///
+/// Internally, each selected value is passed across the Rust FFI as a string.
+/// Values outside the supported set are rejected by the Rust client.
+/// Some Rust-side aliases may also be accepted for compatibility.
+class SnapshotType final : private detail::StringTag<SnapshotType> {

Review Comment:
   done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3293130925


##########
foreign/cpp/src/client.rs:
##########
@@ -604,6 +745,50 @@ impl Client {
             Ok(())
         })
     }
+
+    pub fn heartbeat_interval(&self) -> u64 {
+        // The upstream client exposes this config-derived value via an async API,
+        // so the synchronous C++ wrapper reads it by blocking on the runtime.
+        RUNTIME.block_on(async { self.inner.heartbeat_interval().await.as_micros() })
+    }
+
+    pub fn snapshot(

Review Comment:
   The sdk now rejects empty lists and dedupes when calling rust sdk. Added an e2e test that tests rejection of empty list.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3293195870


##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -150,3 +154,702 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbmR): add a test to create some streams, topics, partitions, and segments, send messages, and create
+// consumer groups and verify it.
+TEST(LowLevelE2E_Client, GetStatsReturnsServerStats) {
+    RecordProperty("description",
+                   "Returns empty resource counts first, then reflects aggregated streams, topics, partitions, "
+                   "consumer groups, and clients.");
+    const std::string first_stream_name                 = "cpp-get-stats-stream-1";
+    const std::string second_stream_name                = "cpp-get-stats-stream-2";
+    const std::string first_topic_name                  = "cpp-get-stats-topic-1";
+    const std::string second_topic_name                 = "cpp-get-stats-topic-2";
+    const std::string third_topic_name                  = "cpp-get-stats-topic-3";
+    const std::string first_group_name                  = "cpp-get-stats-group-1";
+    const std::string second_group_name                 = "cpp-get-stats-group-2";
+    const std::string third_group_name                  = "cpp-get-stats-group-3";
+    constexpr std::uint32_t additional_partitions_count = 2;
+    iggy::ffi::Client *client                           = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Client *second_client = nullptr;
+    iggy::ffi::Client *third_client  = nullptr;
+
+    iggy::ffi::Stats empty_stats{};
+    ASSERT_NO_THROW({
+        empty_stats = client->get_stats();
+        EXPECT_NE(empty_stats.process_id, 0u);
+        EXPECT_GT(empty_stats.threads_count, 0u);
+        EXPECT_GT(empty_stats.total_memory, 0u);
+        EXPECT_LE(empty_stats.available_memory, empty_stats.total_memory);
+        EXPECT_GE(empty_stats.total_disk_space, empty_stats.free_disk_space);
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.hostname).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_name).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.kernel_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.iggy_server_version).empty());
+        EXPECT_TRUE(!empty_stats.has_server_semver || empty_stats.iggy_server_semver > 0u);

Review Comment:
   Removed assertion



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3293204173


##########
foreign/cpp/src/client.rs:
##########
@@ -501,14 +590,12 @@ impl Client {
                 .await
                 .map_err(|error| {
                     format!(
-                        "Could not get consumer group '{}' for topic '{}' on stream '{}': {error}",
-                        rust_group_id, rust_topic_id, rust_stream_id
+                        "Could not get consumer group '{rust_group_id}' for topic '{rust_topic_id}' on stream '{rust_stream_id}': {error}"

Review Comment:
   done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3252467815


##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -149,3 +153,671 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add a test to create some streams, topics, partitions, and segments, send messages, and create
+// consumer groups and verify it.
+TEST(LowLevelE2E_Client, GetStatsReturnsServerStats) {
+    RecordProperty("description",
+                   "Returns empty resource counts first, then reflects aggregated streams, topics, partitions, "
+                   "consumer groups, and clients.");
+    const std::string first_stream_name                 = "cpp-get-stats-stream-1";
+    const std::string second_stream_name                = "cpp-get-stats-stream-2";
+    const std::string first_topic_name                  = "cpp-get-stats-topic-1";
+    const std::string second_topic_name                 = "cpp-get-stats-topic-2";
+    const std::string third_topic_name                  = "cpp-get-stats-topic-3";
+    const std::string first_group_name                  = "cpp-get-stats-group-1";
+    const std::string second_group_name                 = "cpp-get-stats-group-2";
+    const std::string third_group_name                  = "cpp-get-stats-group-3";
+    constexpr std::uint32_t additional_partitions_count = 2;
+    iggy::ffi::Client *client                           = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Client *second_client = nullptr;
+    iggy::ffi::Client *third_client  = nullptr;
+
+    iggy::ffi::Stats empty_stats{};
+    ASSERT_NO_THROW({
+        empty_stats = client->get_stats();
+        EXPECT_NE(empty_stats.process_id, 0u);
+        EXPECT_GT(empty_stats.threads_count, 0u);
+        EXPECT_GT(empty_stats.total_memory, 0u);
+        EXPECT_GE(empty_stats.available_memory, 0u);
+        EXPECT_GE(empty_stats.total_disk_space, empty_stats.free_disk_space);
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.hostname).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_name).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.kernel_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.iggy_server_version).empty());
+        EXPECT_TRUE(empty_stats.iggy_server_semver == std::numeric_limits<std::uint32_t>::max() ||
+                    empty_stats.iggy_server_semver > 0u);
+        EXPECT_GE(empty_stats.cache_metrics.size(), 0u);
+        EXPECT_EQ(empty_stats.streams_count, 0u);
+        EXPECT_EQ(empty_stats.topics_count, 0u);
+        EXPECT_EQ(empty_stats.partitions_count, 0u);
+        EXPECT_EQ(empty_stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(client->create_stream(first_stream_name));
+    ASSERT_NO_THROW(client->create_stream(second_stream_name));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), first_topic_name, 1, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), second_topic_name, 2, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(second_stream_name), third_topic_name, 3, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_partitions(make_string_identifier(first_stream_name),
+                                              make_string_identifier(first_topic_name), additional_partitions_count));
+    const auto first_group  = client->create_consumer_group(make_string_identifier(first_stream_name),
+                                                            make_string_identifier(first_topic_name), first_group_name);
+    const auto second_group = client->create_consumer_group(
+        make_string_identifier(first_stream_name), make_string_identifier(second_topic_name), second_group_name);
+    const auto third_group = client->create_consumer_group(make_string_identifier(second_stream_name),
+                                                           make_string_identifier(third_topic_name), third_group_name);
+
+    ASSERT_NO_THROW({ second_client = login_to_server(); });
+    ASSERT_NE(second_client, nullptr);
+    ASSERT_NO_THROW({ third_client = login_to_server(); });
+    ASSERT_NE(third_client, nullptr);
+
+    const auto first_stream_details           = client->get_stream(make_string_identifier(first_stream_name));
+    const auto second_stream_details          = client->get_stream(make_string_identifier(second_stream_name));
+    const std::uint32_t expected_topics_count = first_stream_details.topics_count + second_stream_details.topics_count;
+    std::uint32_t first_topic_partitions      = 0;
+    std::uint32_t second_topic_partitions     = 0;
+    std::uint32_t third_topic_partitions      = 0;
+    for (const auto &topic : first_stream_details.topics) {
+        if (topic.name == first_topic_name) {
+            first_topic_partitions = topic.partitions_count;
+        }
+        if (topic.name == second_topic_name) {
+            second_topic_partitions = topic.partitions_count;
+        }
+    }
+    for (const auto &topic : second_stream_details.topics) {
+        if (topic.name == third_topic_name) {
+            third_topic_partitions = topic.partitions_count;
+        }
+    }
+    const std::uint32_t expected_partitions_count =
+        first_topic_partitions + second_topic_partitions + third_topic_partitions;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 2u);
+        EXPECT_EQ(stats.topics_count, expected_topics_count);
+        EXPECT_EQ(stats.partitions_count, expected_partitions_count);
+        EXPECT_EQ(stats.segments_count, expected_partitions_count);
+        EXPECT_EQ(stats.consumer_groups_count, 3u);
+        EXPECT_EQ(stats.clients_count, empty_stats.clients_count + 2u);
+        EXPECT_EQ(first_group.partitions_count, first_topic_partitions);
+        EXPECT_EQ(second_group.partitions_count, second_topic_partitions);
+        EXPECT_EQ(third_group.partitions_count, third_topic_partitions);
+    });
+
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(second_stream_name)));
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(first_stream_name)));
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(third_client));
+    third_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 0u);
+        EXPECT_EQ(stats.topics_count, 0u);
+        EXPECT_EQ(stats.partitions_count, 0u);
+        EXPECT_EQ(stats.segments_count, 0u);
+        EXPECT_EQ(stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetStatsIsStableAcrossBackToBackCalls) {
+    RecordProperty(
+        "description",
+        "Returns sane invariant fields across back-to-back get_stats calls on an idle authenticated client.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Stats first_stats{};
+    iggy::ffi::Stats second_stats{};
+    ASSERT_NO_THROW({
+        first_stats  = client->get_stats();
+        second_stats = client->get_stats();
+    });
+
+    EXPECT_NE(first_stats.process_id, 0u);
+    EXPECT_NE(second_stats.process_id, 0u);
+    EXPECT_EQ(second_stats.process_id, first_stats.process_id);
+    EXPECT_GT(first_stats.threads_count, 0u);
+    EXPECT_GT(second_stats.threads_count, 0u);
+    EXPECT_GT(first_stats.total_memory, 0u);
+    EXPECT_GT(second_stats.total_memory, 0u);
+    EXPECT_FALSE(static_cast<std::string>(first_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.iggy_server_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.iggy_server_version).empty());
+    EXPECT_EQ(static_cast<std::string>(second_stats.hostname), static_cast<std::string>(first_stats.hostname));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_name), static_cast<std::string>(first_stats.os_name));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_version), static_cast<std::string>(first_stats.os_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.kernel_version),
+              static_cast<std::string>(first_stats.kernel_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.iggy_server_version),
+              static_cast<std::string>(first_stats.iggy_server_version));
+    EXPECT_EQ(second_stats.iggy_server_semver, first_stats.iggy_server_semver);
+    EXPECT_EQ(second_stats.clients_count, first_stats.clients_count);
+    EXPECT_EQ(second_stats.streams_count, first_stats.streams_count);
+    EXPECT_EQ(second_stats.topics_count, first_stats.topics_count);
+    EXPECT_EQ(second_stats.partitions_count, first_stats.partitions_count);
+    EXPECT_EQ(second_stats.consumer_groups_count, first_stats.consumer_groups_count);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_me before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_me(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_me(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add additional validation for get_me after merging join_consumer_group PR.
+TEST(LowLevelE2E_Client, GetMeReturnsCurrentClientDetails) {
+    RecordProperty("description", "Returns the current authenticated client details.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_NO_THROW({
+        const auto me = client->get_me();
+        EXPECT_NE(me.client_id, 0u);
+        EXPECT_NE(me.user_id, std::numeric_limits<std::uint32_t>::max());

Review Comment:
   converted to EXPECT_TRUE(me.has_user_id)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "hubcio (via GitHub)" <gi...@apache.org>.
hubcio commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3160952530


##########
foreign/cpp/src/client.rs:
##########
@@ -20,11 +20,112 @@ use iggy::prelude::{
     Client as IggyConnectionClient, CompressionAlgorithm as RustCompressionAlgorithm,
     ConsumerGroupClient, Identifier as RustIdentifier, IggyClient as RustIggyClient,
     IggyClientBuilder as RustIggyClientBuilder, IggyError, IggyExpiry as RustIggyExpiry,
-    MaxTopicSize as RustMaxTopicSize, PartitionClient, StreamClient, TopicClient, UserClient,
+    MaxTopicSize as RustMaxTopicSize, PartitionClient,
+    SnapshotCompression as RustSnapshotCompression, StreamClient, SystemClient as RustSystemClient,
+    SystemSnapshotType as RustSystemSnapshotType, TopicClient, UserClient,
 };
+use iggy_common::{
+    CacheMetrics as RustCacheMetrics, CacheMetricsKey as RustCacheMetricsKey,
+    ClientInfo as RustClientInfo, ClientInfoDetails as RustClientInfoDetails, Stats as RustStats,
+};
+use std::convert::TryFrom;
 use std::str::FromStr;
 use std::sync::Arc;
 
+impl From<RustClientInfo> for ffi::ClientInfo {
+    fn from(client: RustClientInfo) -> Self {
+        ffi::ClientInfo {
+            client_id: client.client_id,
+            // TODO(slbotbm): In high-level client, this should be converted to None.
+            user_id: client.user_id.unwrap_or(u32::MAX),

Review Comment:
   `client.user_id.unwrap_or(u32::MAX)` collapses `None` and a valid id of `u32::MAX` into the same value. backing type is `Option<u32>` (core/common/src/types/client/client_info.rs:33). plumb option through ffi - add `has_user_id: bool` to `ffi::ClientInfo` or split into a shared cxx option type.



##########
foreign/cpp/src/client.rs:
##########
@@ -20,11 +20,112 @@ use iggy::prelude::{
     Client as IggyConnectionClient, CompressionAlgorithm as RustCompressionAlgorithm,
     ConsumerGroupClient, Identifier as RustIdentifier, IggyClient as RustIggyClient,
     IggyClientBuilder as RustIggyClientBuilder, IggyError, IggyExpiry as RustIggyExpiry,
-    MaxTopicSize as RustMaxTopicSize, PartitionClient, StreamClient, TopicClient, UserClient,
+    MaxTopicSize as RustMaxTopicSize, PartitionClient,
+    SnapshotCompression as RustSnapshotCompression, StreamClient, SystemClient as RustSystemClient,
+    SystemSnapshotType as RustSystemSnapshotType, TopicClient, UserClient,
 };
+use iggy_common::{
+    CacheMetrics as RustCacheMetrics, CacheMetricsKey as RustCacheMetricsKey,
+    ClientInfo as RustClientInfo, ClientInfoDetails as RustClientInfoDetails, Stats as RustStats,
+};
+use std::convert::TryFrom;
 use std::str::FromStr;
 use std::sync::Arc;
 
+impl From<RustClientInfo> for ffi::ClientInfo {
+    fn from(client: RustClientInfo) -> Self {
+        ffi::ClientInfo {
+            client_id: client.client_id,
+            // TODO(slbotbm): In high-level client, this should be converted to None.
+            user_id: client.user_id.unwrap_or(u32::MAX),
+            address: client.address,
+            transport: client.transport,
+            consumer_groups_count: client.consumer_groups_count,
+        }
+    }
+}
+
+impl From<RustClientInfoDetails> for ffi::ClientInfoDetails {
+    fn from(client: RustClientInfoDetails) -> Self {
+        ffi::ClientInfoDetails {
+            client_id: client.client_id,
+            // TODO(slbotbm): In high-level client, this should be converted to None.
+            user_id: client.user_id.unwrap_or(u32::MAX),

Review Comment:
   same sentinel collision in `From<RustClientInfoDetails>`. same fix as L40.



##########
foreign/cpp/src/lib.rs:
##########
@@ -75,6 +75,69 @@ mod ffi {
         members: Vec<ConsumerGroupMember>,
     }
 
+    struct ConsumerGroupInfo {
+        stream_id: u32,
+        topic_id: u32,
+        group_id: u32,
+    }
+
+    struct ClientInfo {
+        client_id: u32,
+        user_id: u32,

Review Comment:
   `user_id: u32` in `ffi::ClientInfo` cannot represent an absent value. root cause behind client.rs:40. add `has_user_id: bool` field.



##########
foreign/cpp/src/lib.rs:
##########
@@ -75,6 +75,69 @@ mod ffi {
         members: Vec<ConsumerGroupMember>,
     }
 
+    struct ConsumerGroupInfo {
+        stream_id: u32,
+        topic_id: u32,
+        group_id: u32,
+    }
+
+    struct ClientInfo {
+        client_id: u32,
+        user_id: u32,
+        address: String,
+        transport: String,
+        consumer_groups_count: u32,
+    }
+
+    struct ClientInfoDetails {
+        client_id: u32,
+        user_id: u32,

Review Comment:
   same shape problem for `ffi::ClientInfoDetails.user_id`.



##########
foreign/cpp/src/lib.rs:
##########
@@ -75,6 +75,69 @@ mod ffi {
         members: Vec<ConsumerGroupMember>,
     }
 
+    struct ConsumerGroupInfo {
+        stream_id: u32,
+        topic_id: u32,
+        group_id: u32,
+    }
+
+    struct ClientInfo {
+        client_id: u32,
+        user_id: u32,
+        address: String,
+        transport: String,
+        consumer_groups_count: u32,
+    }
+
+    struct ClientInfoDetails {
+        client_id: u32,
+        user_id: u32,
+        address: String,
+        transport: String,
+        consumer_groups_count: u32,
+        consumer_groups: Vec<ConsumerGroupInfo>,
+    }
+
+    struct CacheMetricEntry {
+        stream_id: u32,
+        topic_id: u32,
+        partition_id: u32,
+        hits: u64,
+        misses: u64,
+        hit_ratio: f32,
+    }
+
+    struct Stats {
+        process_id: u32,
+        cpu_usage: f32,
+        total_cpu_usage: f32,
+        memory_usage: u64,
+        total_memory: u64,
+        available_memory: u64,
+        run_time: u64,
+        start_time: u64,
+        read_bytes: u64,
+        written_bytes: u64,
+        messages_size_bytes: u64,
+        streams_count: u32,
+        topics_count: u32,
+        partitions_count: u32,
+        segments_count: u32,
+        messages_count: u64,
+        clients_count: u32,
+        consumer_groups_count: u32,
+        hostname: String,
+        os_name: String,
+        os_version: String,
+        kernel_version: String,
+        iggy_server_version: String,
+        iggy_server_semver: u32,

Review Comment:
   `iggy_server_semver: u32` paired with `unwrap_or(u32::MAX)` sentinel at client.rs:116. backing field is `Option<u32>` (core/common/src/types/stats/mod.rs:73). prior commit #3036 already got burned by a zero-sentinel - don't repeat. add `has_server_semver: bool`.



##########
foreign/cpp/src/client.rs:
##########
@@ -378,6 +479,98 @@ impl Client {
             Ok(())
         })
     }
+
+    pub fn get_stats(&self) -> Result<ffi::Stats, String> {
+        RUNTIME.block_on(async {
+            let stats = self
+                .inner
+                .get_stats()
+                .await
+                .map_err(|error| format!("Could not get stats: {error}"))?;
+            Ok(ffi::Stats::from(stats))
+        })
+    }
+
+    pub fn get_me(&self) -> Result<ffi::ClientInfoDetails, String> {
+        RUNTIME.block_on(async {
+            let client = self
+                .inner
+                .get_me()
+                .await
+                .map_err(|error| format!("Could not get current client info: {error}"))?;
+            Ok(ffi::ClientInfoDetails::from(client))
+        })
+    }
+
+    pub fn get_client(&self, client_id: u32) -> Result<ffi::ClientInfoDetails, String> {
+        RUNTIME.block_on(async {
+            let client = self
+                .inner
+                .get_client(client_id)
+                .await
+                .map_err(|error| format!("Could not get client '{client_id}': {error}"))?;
+            ffi::ClientInfoDetails::try_from(client)
+                .map_err(|error| format!("Could not get client '{client_id}': {error}"))
+        })
+    }
+
+    pub fn get_clients(&self) -> Result<Vec<ffi::ClientInfo>, String> {
+        RUNTIME.block_on(async {
+            let clients = self
+                .inner
+                .get_clients()
+                .await
+                .map_err(|error| format!("Could not get clients: {error}"))?;
+            Ok(clients.into_iter().map(ffi::ClientInfo::from).collect())
+        })
+    }
+
+    pub fn ping(&self) -> Result<(), String> {
+        RUNTIME.block_on(async {
+            self.inner
+                .ping()
+                .await
+                .map_err(|error| format!("Could not ping server: {error}"))?;
+            Ok(())
+        })
+    }
+
+    pub fn heartbeat_interval(&self) -> u64 {
+        RUNTIME.block_on(async { self.inner.heartbeat_interval().await.as_micros() })
+    }
+
+    pub fn snapshot(
+        &self,
+        snapshot_compression: String,
+        snapshot_types: Vec<String>,
+    ) -> Result<Vec<u8>, String> {
+        let rust_compression = match snapshot_compression.trim() {
+            "" => RustSnapshotCompression::default(),

Review Comment:
   empty `snapshot_compression` silently falls back to `RustSnapshotCompression::default()`. either reject empty (consistent with the hpp factory which always emits a non-empty value) or document the `"" == default` contract.



##########
foreign/cpp/src/client.rs:
##########
@@ -378,6 +479,98 @@ impl Client {
             Ok(())
         })
     }
+
+    pub fn get_stats(&self) -> Result<ffi::Stats, String> {
+        RUNTIME.block_on(async {
+            let stats = self
+                .inner
+                .get_stats()
+                .await
+                .map_err(|error| format!("Could not get stats: {error}"))?;
+            Ok(ffi::Stats::from(stats))
+        })
+    }
+
+    pub fn get_me(&self) -> Result<ffi::ClientInfoDetails, String> {
+        RUNTIME.block_on(async {
+            let client = self
+                .inner
+                .get_me()
+                .await
+                .map_err(|error| format!("Could not get current client info: {error}"))?;
+            Ok(ffi::ClientInfoDetails::from(client))
+        })
+    }
+
+    pub fn get_client(&self, client_id: u32) -> Result<ffi::ClientInfoDetails, String> {
+        RUNTIME.block_on(async {
+            let client = self
+                .inner
+                .get_client(client_id)
+                .await
+                .map_err(|error| format!("Could not get client '{client_id}': {error}"))?;
+            ffi::ClientInfoDetails::try_from(client)
+                .map_err(|error| format!("Could not get client '{client_id}': {error}"))
+        })
+    }
+
+    pub fn get_clients(&self) -> Result<Vec<ffi::ClientInfo>, String> {
+        RUNTIME.block_on(async {
+            let clients = self
+                .inner
+                .get_clients()
+                .await
+                .map_err(|error| format!("Could not get clients: {error}"))?;
+            Ok(clients.into_iter().map(ffi::ClientInfo::from).collect())
+        })
+    }
+
+    pub fn ping(&self) -> Result<(), String> {
+        RUNTIME.block_on(async {
+            self.inner
+                .ping()
+                .await
+                .map_err(|error| format!("Could not ping server: {error}"))?;
+            Ok(())
+        })
+    }
+
+    pub fn heartbeat_interval(&self) -> u64 {
+        RUNTIME.block_on(async { self.inner.heartbeat_interval().await.as_micros() })

Review Comment:
   `RUNTIME.block_on` for `heartbeat_interval` reads a config-shaped value. acceptable since the upstream method is async, but worth an inline comment so future readers don't suspect a bug.



##########
foreign/cpp/src/client.rs:
##########
@@ -378,6 +479,98 @@ impl Client {
             Ok(())
         })
     }
+
+    pub fn get_stats(&self) -> Result<ffi::Stats, String> {
+        RUNTIME.block_on(async {
+            let stats = self
+                .inner
+                .get_stats()
+                .await
+                .map_err(|error| format!("Could not get stats: {error}"))?;
+            Ok(ffi::Stats::from(stats))
+        })
+    }
+
+    pub fn get_me(&self) -> Result<ffi::ClientInfoDetails, String> {
+        RUNTIME.block_on(async {
+            let client = self
+                .inner
+                .get_me()
+                .await
+                .map_err(|error| format!("Could not get current client info: {error}"))?;
+            Ok(ffi::ClientInfoDetails::from(client))
+        })
+    }
+
+    pub fn get_client(&self, client_id: u32) -> Result<ffi::ClientInfoDetails, String> {
+        RUNTIME.block_on(async {
+            let client = self
+                .inner
+                .get_client(client_id)
+                .await
+                .map_err(|error| format!("Could not get client '{client_id}': {error}"))?;
+            ffi::ClientInfoDetails::try_from(client)
+                .map_err(|error| format!("Could not get client '{client_id}': {error}"))
+        })
+    }
+
+    pub fn get_clients(&self) -> Result<Vec<ffi::ClientInfo>, String> {
+        RUNTIME.block_on(async {
+            let clients = self
+                .inner
+                .get_clients()
+                .await
+                .map_err(|error| format!("Could not get clients: {error}"))?;
+            Ok(clients.into_iter().map(ffi::ClientInfo::from).collect())
+        })
+    }
+
+    pub fn ping(&self) -> Result<(), String> {
+        RUNTIME.block_on(async {
+            self.inner
+                .ping()
+                .await
+                .map_err(|error| format!("Could not ping server: {error}"))?;
+            Ok(())
+        })
+    }
+
+    pub fn heartbeat_interval(&self) -> u64 {
+        RUNTIME.block_on(async { self.inner.heartbeat_interval().await.as_micros() })
+    }
+
+    pub fn snapshot(
+        &self,
+        snapshot_compression: String,
+        snapshot_types: Vec<String>,
+    ) -> Result<Vec<u8>, String> {
+        let rust_compression = match snapshot_compression.trim() {
+            "" => RustSnapshotCompression::default(),
+            value => RustSnapshotCompression::from_str(value).map_err(|error| {
+                format!("Could not capture snapshot: invalid compression '{value}': {error}")
+            })?,
+        };
+        let rust_snapshot_types = snapshot_types
+            .into_iter()
+            .map(|snapshot_type| {
+                RustSystemSnapshotType::from_str(&snapshot_type).map_err(|error| {
+                    format!(
+                        "Could not capture snapshot: invalid snapshot type '{}': {}",
+                        snapshot_type, error
+                    )
+                })
+            })
+            .collect::<Result<Vec<_>, _>>()?;
+
+        RUNTIME.block_on(async {
+            let snapshot = self
+                .inner
+                .snapshot(rust_compression, rust_snapshot_types)
+                .await
+                .map_err(|error| format!("Could not capture snapshot: {error}"))?;
+            Ok(snapshot.0)

Review Comment:
   `Ok(snapshot.0)` is an opaque tuple-struct index. `let Snapshot(bytes) = snapshot; Ok(bytes)` reads better.



##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -149,3 +153,671 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add a test to create some streams, topics, partitions, and segments, send messages, and create

Review Comment:
   `TODO(slbotbm-this PR)` self-declares as blocking on this PR. resolve here or strip the `-this PR` suffix before merge.



##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -149,3 +153,671 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add a test to create some streams, topics, partitions, and segments, send messages, and create
+// consumer groups and verify it.
+TEST(LowLevelE2E_Client, GetStatsReturnsServerStats) {
+    RecordProperty("description",
+                   "Returns empty resource counts first, then reflects aggregated streams, topics, partitions, "
+                   "consumer groups, and clients.");
+    const std::string first_stream_name                 = "cpp-get-stats-stream-1";
+    const std::string second_stream_name                = "cpp-get-stats-stream-2";
+    const std::string first_topic_name                  = "cpp-get-stats-topic-1";
+    const std::string second_topic_name                 = "cpp-get-stats-topic-2";
+    const std::string third_topic_name                  = "cpp-get-stats-topic-3";
+    const std::string first_group_name                  = "cpp-get-stats-group-1";
+    const std::string second_group_name                 = "cpp-get-stats-group-2";
+    const std::string third_group_name                  = "cpp-get-stats-group-3";
+    constexpr std::uint32_t additional_partitions_count = 2;
+    iggy::ffi::Client *client                           = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Client *second_client = nullptr;
+    iggy::ffi::Client *third_client  = nullptr;
+
+    iggy::ffi::Stats empty_stats{};
+    ASSERT_NO_THROW({
+        empty_stats = client->get_stats();
+        EXPECT_NE(empty_stats.process_id, 0u);
+        EXPECT_GT(empty_stats.threads_count, 0u);
+        EXPECT_GT(empty_stats.total_memory, 0u);
+        EXPECT_GE(empty_stats.available_memory, 0u);
+        EXPECT_GE(empty_stats.total_disk_space, empty_stats.free_disk_space);
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.hostname).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_name).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.kernel_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.iggy_server_version).empty());
+        EXPECT_TRUE(empty_stats.iggy_server_semver == std::numeric_limits<std::uint32_t>::max() ||
+                    empty_stats.iggy_server_semver > 0u);
+        EXPECT_GE(empty_stats.cache_metrics.size(), 0u);
+        EXPECT_EQ(empty_stats.streams_count, 0u);

Review Comment:
   absolute counts (`streams_count == 0u`, `topics_count == 0u` here, `consumer_groups_count == 3u` at L264, the full block at L280-285) assume a fresh, single-process server. replace with deltas vs the `empty_stats` baseline already captured at L194.



##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -149,3 +153,671 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add a test to create some streams, topics, partitions, and segments, send messages, and create
+// consumer groups and verify it.
+TEST(LowLevelE2E_Client, GetStatsReturnsServerStats) {
+    RecordProperty("description",
+                   "Returns empty resource counts first, then reflects aggregated streams, topics, partitions, "
+                   "consumer groups, and clients.");
+    const std::string first_stream_name                 = "cpp-get-stats-stream-1";
+    const std::string second_stream_name                = "cpp-get-stats-stream-2";
+    const std::string first_topic_name                  = "cpp-get-stats-topic-1";
+    const std::string second_topic_name                 = "cpp-get-stats-topic-2";
+    const std::string third_topic_name                  = "cpp-get-stats-topic-3";
+    const std::string first_group_name                  = "cpp-get-stats-group-1";
+    const std::string second_group_name                 = "cpp-get-stats-group-2";
+    const std::string third_group_name                  = "cpp-get-stats-group-3";
+    constexpr std::uint32_t additional_partitions_count = 2;
+    iggy::ffi::Client *client                           = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Client *second_client = nullptr;
+    iggy::ffi::Client *third_client  = nullptr;
+
+    iggy::ffi::Stats empty_stats{};
+    ASSERT_NO_THROW({
+        empty_stats = client->get_stats();
+        EXPECT_NE(empty_stats.process_id, 0u);
+        EXPECT_GT(empty_stats.threads_count, 0u);
+        EXPECT_GT(empty_stats.total_memory, 0u);
+        EXPECT_GE(empty_stats.available_memory, 0u);

Review Comment:
   `EXPECT_GE(empty_stats.available_memory, 0u)` is a tautology - `available_memory` is `u64`, so `>= 0u` always holds. drop or replace with `EXPECT_LE(available_memory, total_memory)`.



##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -149,3 +153,671 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add a test to create some streams, topics, partitions, and segments, send messages, and create
+// consumer groups and verify it.
+TEST(LowLevelE2E_Client, GetStatsReturnsServerStats) {
+    RecordProperty("description",
+                   "Returns empty resource counts first, then reflects aggregated streams, topics, partitions, "
+                   "consumer groups, and clients.");
+    const std::string first_stream_name                 = "cpp-get-stats-stream-1";
+    const std::string second_stream_name                = "cpp-get-stats-stream-2";
+    const std::string first_topic_name                  = "cpp-get-stats-topic-1";
+    const std::string second_topic_name                 = "cpp-get-stats-topic-2";
+    const std::string third_topic_name                  = "cpp-get-stats-topic-3";
+    const std::string first_group_name                  = "cpp-get-stats-group-1";
+    const std::string second_group_name                 = "cpp-get-stats-group-2";
+    const std::string third_group_name                  = "cpp-get-stats-group-3";
+    constexpr std::uint32_t additional_partitions_count = 2;
+    iggy::ffi::Client *client                           = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Client *second_client = nullptr;
+    iggy::ffi::Client *third_client  = nullptr;
+
+    iggy::ffi::Stats empty_stats{};
+    ASSERT_NO_THROW({
+        empty_stats = client->get_stats();
+        EXPECT_NE(empty_stats.process_id, 0u);
+        EXPECT_GT(empty_stats.threads_count, 0u);
+        EXPECT_GT(empty_stats.total_memory, 0u);
+        EXPECT_GE(empty_stats.available_memory, 0u);
+        EXPECT_GE(empty_stats.total_disk_space, empty_stats.free_disk_space);
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.hostname).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_name).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.kernel_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.iggy_server_version).empty());
+        EXPECT_TRUE(empty_stats.iggy_server_semver == std::numeric_limits<std::uint32_t>::max() ||
+                    empty_stats.iggy_server_semver > 0u);
+        EXPECT_GE(empty_stats.cache_metrics.size(), 0u);

Review Comment:
   `EXPECT_GE(empty_stats.cache_metrics.size(), 0u)` is tautological for `size_t`. drop.



##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -149,3 +153,671 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));

Review Comment:
   every test manually does `delete_connection(client); client = nullptr;`. wrap in `struct ScopedClient { ~ScopedClient() { iggy::ffi::delete_connection(p); } };` to cut ~30 lines of boilerplate and recover correctness on test-assertion failure - today the connection leaks if an `ASSERT_*` aborts mid-test.



##########
foreign/cpp/build.rs:
##########
@@ -25,5 +25,6 @@ fn main() {
     println!("cargo:rerun-if-changed=src/identifier.rs");
     println!("cargo:rerun-if-changed=src/lib.rs");
     println!("cargo:rerun-if-changed=src/stream.rs");
+    println!("cargo:rerun-if-changed=src/system.rs");

Review Comment:
   `cargo:rerun-if-changed=src/system.rs` watches a file that doesn't exist (verified `ls foreign/cpp/src/system.rs`). cargo accepts the entry but it's dead config. delete the line or create the file.



##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -149,3 +153,671 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add a test to create some streams, topics, partitions, and segments, send messages, and create
+// consumer groups and verify it.
+TEST(LowLevelE2E_Client, GetStatsReturnsServerStats) {
+    RecordProperty("description",
+                   "Returns empty resource counts first, then reflects aggregated streams, topics, partitions, "
+                   "consumer groups, and clients.");
+    const std::string first_stream_name                 = "cpp-get-stats-stream-1";
+    const std::string second_stream_name                = "cpp-get-stats-stream-2";
+    const std::string first_topic_name                  = "cpp-get-stats-topic-1";
+    const std::string second_topic_name                 = "cpp-get-stats-topic-2";
+    const std::string third_topic_name                  = "cpp-get-stats-topic-3";
+    const std::string first_group_name                  = "cpp-get-stats-group-1";
+    const std::string second_group_name                 = "cpp-get-stats-group-2";
+    const std::string third_group_name                  = "cpp-get-stats-group-3";
+    constexpr std::uint32_t additional_partitions_count = 2;
+    iggy::ffi::Client *client                           = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Client *second_client = nullptr;
+    iggy::ffi::Client *third_client  = nullptr;
+
+    iggy::ffi::Stats empty_stats{};
+    ASSERT_NO_THROW({
+        empty_stats = client->get_stats();
+        EXPECT_NE(empty_stats.process_id, 0u);
+        EXPECT_GT(empty_stats.threads_count, 0u);
+        EXPECT_GT(empty_stats.total_memory, 0u);
+        EXPECT_GE(empty_stats.available_memory, 0u);
+        EXPECT_GE(empty_stats.total_disk_space, empty_stats.free_disk_space);
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.hostname).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_name).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.kernel_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.iggy_server_version).empty());
+        EXPECT_TRUE(empty_stats.iggy_server_semver == std::numeric_limits<std::uint32_t>::max() ||
+                    empty_stats.iggy_server_semver > 0u);
+        EXPECT_GE(empty_stats.cache_metrics.size(), 0u);
+        EXPECT_EQ(empty_stats.streams_count, 0u);
+        EXPECT_EQ(empty_stats.topics_count, 0u);
+        EXPECT_EQ(empty_stats.partitions_count, 0u);
+        EXPECT_EQ(empty_stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(client->create_stream(first_stream_name));
+    ASSERT_NO_THROW(client->create_stream(second_stream_name));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), first_topic_name, 1, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), second_topic_name, 2, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(second_stream_name), third_topic_name, 3, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_partitions(make_string_identifier(first_stream_name),
+                                              make_string_identifier(first_topic_name), additional_partitions_count));
+    const auto first_group  = client->create_consumer_group(make_string_identifier(first_stream_name),
+                                                            make_string_identifier(first_topic_name), first_group_name);
+    const auto second_group = client->create_consumer_group(
+        make_string_identifier(first_stream_name), make_string_identifier(second_topic_name), second_group_name);
+    const auto third_group = client->create_consumer_group(make_string_identifier(second_stream_name),
+                                                           make_string_identifier(third_topic_name), third_group_name);
+
+    ASSERT_NO_THROW({ second_client = login_to_server(); });
+    ASSERT_NE(second_client, nullptr);
+    ASSERT_NO_THROW({ third_client = login_to_server(); });
+    ASSERT_NE(third_client, nullptr);
+
+    const auto first_stream_details           = client->get_stream(make_string_identifier(first_stream_name));
+    const auto second_stream_details          = client->get_stream(make_string_identifier(second_stream_name));
+    const std::uint32_t expected_topics_count = first_stream_details.topics_count + second_stream_details.topics_count;
+    std::uint32_t first_topic_partitions      = 0;
+    std::uint32_t second_topic_partitions     = 0;
+    std::uint32_t third_topic_partitions      = 0;
+    for (const auto &topic : first_stream_details.topics) {
+        if (topic.name == first_topic_name) {
+            first_topic_partitions = topic.partitions_count;
+        }
+        if (topic.name == second_topic_name) {
+            second_topic_partitions = topic.partitions_count;
+        }
+    }
+    for (const auto &topic : second_stream_details.topics) {
+        if (topic.name == third_topic_name) {
+            third_topic_partitions = topic.partitions_count;
+        }
+    }
+    const std::uint32_t expected_partitions_count =
+        first_topic_partitions + second_topic_partitions + third_topic_partitions;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 2u);
+        EXPECT_EQ(stats.topics_count, expected_topics_count);
+        EXPECT_EQ(stats.partitions_count, expected_partitions_count);
+        EXPECT_EQ(stats.segments_count, expected_partitions_count);
+        EXPECT_EQ(stats.consumer_groups_count, 3u);
+        EXPECT_EQ(stats.clients_count, empty_stats.clients_count + 2u);
+        EXPECT_EQ(first_group.partitions_count, first_topic_partitions);
+        EXPECT_EQ(second_group.partitions_count, second_topic_partitions);
+        EXPECT_EQ(third_group.partitions_count, third_topic_partitions);
+    });
+
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(second_stream_name)));
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(first_stream_name)));
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(third_client));
+    third_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 0u);
+        EXPECT_EQ(stats.topics_count, 0u);
+        EXPECT_EQ(stats.partitions_count, 0u);
+        EXPECT_EQ(stats.segments_count, 0u);
+        EXPECT_EQ(stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetStatsIsStableAcrossBackToBackCalls) {
+    RecordProperty(
+        "description",
+        "Returns sane invariant fields across back-to-back get_stats calls on an idle authenticated client.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Stats first_stats{};
+    iggy::ffi::Stats second_stats{};
+    ASSERT_NO_THROW({
+        first_stats  = client->get_stats();
+        second_stats = client->get_stats();
+    });
+
+    EXPECT_NE(first_stats.process_id, 0u);
+    EXPECT_NE(second_stats.process_id, 0u);
+    EXPECT_EQ(second_stats.process_id, first_stats.process_id);
+    EXPECT_GT(first_stats.threads_count, 0u);
+    EXPECT_GT(second_stats.threads_count, 0u);
+    EXPECT_GT(first_stats.total_memory, 0u);
+    EXPECT_GT(second_stats.total_memory, 0u);
+    EXPECT_FALSE(static_cast<std::string>(first_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.iggy_server_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.iggy_server_version).empty());
+    EXPECT_EQ(static_cast<std::string>(second_stats.hostname), static_cast<std::string>(first_stats.hostname));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_name), static_cast<std::string>(first_stats.os_name));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_version), static_cast<std::string>(first_stats.os_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.kernel_version),
+              static_cast<std::string>(first_stats.kernel_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.iggy_server_version),
+              static_cast<std::string>(first_stats.iggy_server_version));
+    EXPECT_EQ(second_stats.iggy_server_semver, first_stats.iggy_server_semver);
+    EXPECT_EQ(second_stats.clients_count, first_stats.clients_count);
+    EXPECT_EQ(second_stats.streams_count, first_stats.streams_count);
+    EXPECT_EQ(second_stats.topics_count, first_stats.topics_count);
+    EXPECT_EQ(second_stats.partitions_count, first_stats.partitions_count);
+    EXPECT_EQ(second_stats.consumer_groups_count, first_stats.consumer_groups_count);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_me before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_me(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_me(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add additional validation for get_me after merging join_consumer_group PR.
+TEST(LowLevelE2E_Client, GetMeReturnsCurrentClientDetails) {
+    RecordProperty("description", "Returns the current authenticated client details.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_NO_THROW({
+        const auto me = client->get_me();
+        EXPECT_NE(me.client_id, 0u);
+        EXPECT_NE(me.user_id, std::numeric_limits<std::uint32_t>::max());
+        EXPECT_FALSE(static_cast<std::string>(me.address).empty());
+        EXPECT_EQ(static_cast<std::string>(me.transport), "TCP");
+        EXPECT_EQ(me.consumer_groups_count, 0u);
+        EXPECT_TRUE(me.consumer_groups.empty());
+    });
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeIsStableAcrossBackToBackCalls) {
+    RecordProperty("description", "Returns stable current-client details across back-to-back get_me calls.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({
+        first_me  = client->get_me();
+        second_me = client->get_me();
+    });
+
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_EQ(second_me.client_id, first_me.client_id);
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(second_me.address), static_cast<std::string>(first_me.address));
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), static_cast<std::string>(first_me.transport));
+    EXPECT_EQ(second_me.consumer_groups_count, first_me.consumer_groups_count);
+    EXPECT_EQ(second_me.consumer_groups.size(), first_me.consumer_groups.size());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeReturnsDistinctClientIdsForDifferentSessions) {
+    RecordProperty(
+        "description",
+        "Returns different client ids for separate authenticated sessions while keeping the same user identity.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({
+        first_me  = first_client->get_me();
+        second_me = second_client->get_me();
+    });
+
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_NE(second_me.client_id, 0u);
+    EXPECT_NE(second_me.client_id, first_me.client_id);
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeReturnsValidDetailsAfterReconnect) {
+    RecordProperty("description",
+                   "Returns valid current-client details after reconnecting with a fresh authenticated session.");
+    iggy::ffi::Client *first_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    ASSERT_NO_THROW({ first_me = first_client->get_me(); });
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_NE(first_me.user_id, std::numeric_limits<std::uint32_t>::max());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({ second_me = second_client->get_me(); });
+    EXPECT_NE(second_me.client_id, 0u);
+    EXPECT_NE(second_me.user_id, std::numeric_limits<std::uint32_t>::max());
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), static_cast<std::string>(first_me.transport));
+    EXPECT_FALSE(static_cast<std::string>(second_me.address).empty());
+    EXPECT_EQ(second_me.consumer_groups_count, 0u);
+    EXPECT_TRUE(second_me.consumer_groups.empty());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_client before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_client(1), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_client(1), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientWithWrongClientIdThrows) {
+    RecordProperty("description", "Rejects querying invalid or non-existent client ids.");
+    const std::uint32_t wrong_client_ids[] = {0u, std::numeric_limits<std::uint32_t>::max()};
+    iggy::ffi::Client *client              = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    for (const std::uint32_t wrong_client_id : wrong_client_ids) {
+        SCOPED_TRACE(wrong_client_id);
+        ASSERT_THROW(client->get_client(wrong_client_id), std::exception);
+    }
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientReturnsDetailsForMatchingClientId) {
+    RecordProperty("description", "Returns current client details when querying with the authenticated client id.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::ClientInfoDetails current_client{};
+    iggy::ffi::ClientInfoDetails looked_up_client{};
+    ASSERT_NO_THROW({
+        current_client   = client->get_me();
+        looked_up_client = client->get_client(current_client.client_id);
+    });
+
+    EXPECT_NE(current_client.client_id, 0u);
+    EXPECT_EQ(looked_up_client.client_id, current_client.client_id);
+    EXPECT_EQ(looked_up_client.user_id, current_client.user_id);
+    EXPECT_EQ(static_cast<std::string>(looked_up_client.address), static_cast<std::string>(current_client.address));
+    EXPECT_EQ(static_cast<std::string>(looked_up_client.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(looked_up_client.transport), static_cast<std::string>(current_client.transport));
+    EXPECT_EQ(looked_up_client.consumer_groups_count, current_client.consumer_groups_count);
+    EXPECT_EQ(looked_up_client.consumer_groups.size(), current_client.consumer_groups.size());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientIsStableAcrossBackToBackCalls) {
+    RecordProperty("description", "Returns stable client details across back-to-back get_client calls.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::ClientInfoDetails current_client{};
+    iggy::ffi::ClientInfoDetails first_lookup{};
+    iggy::ffi::ClientInfoDetails second_lookup{};
+    ASSERT_NO_THROW({
+        current_client = client->get_me();
+        first_lookup   = client->get_client(current_client.client_id);
+        second_lookup  = client->get_client(current_client.client_id);
+    });
+
+    EXPECT_NE(current_client.client_id, 0u);
+    EXPECT_EQ(first_lookup.client_id, current_client.client_id);
+    EXPECT_EQ(second_lookup.client_id, first_lookup.client_id);
+    EXPECT_EQ(second_lookup.user_id, first_lookup.user_id);
+    EXPECT_EQ(static_cast<std::string>(second_lookup.address), static_cast<std::string>(first_lookup.address));
+    EXPECT_EQ(static_cast<std::string>(first_lookup.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_lookup.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_lookup.transport), static_cast<std::string>(first_lookup.transport));
+    EXPECT_EQ(second_lookup.consumer_groups_count, first_lookup.consumer_groups_count);
+    EXPECT_EQ(second_lookup.consumer_groups.size(), first_lookup.consumer_groups.size());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_clients before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_clients(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_clients(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsReturnsActiveClientSessions) {
+    RecordProperty("description", "Returns the currently active authenticated client sessions.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails second_me{};
+    rust::Vec<iggy::ffi::ClientInfo> clients;
+    ASSERT_NO_THROW({
+        first_me  = first_client->get_me();
+        second_me = second_client->get_me();
+        clients   = first_client->get_clients();
+    });
+
+    ASSERT_GE(clients.size(), 2u);
+
+    bool found_first  = false;
+    bool found_second = false;
+    for (const auto &client : clients) {
+        EXPECT_NE(client.client_id, 0u);
+        EXPECT_EQ(static_cast<std::string>(client.transport), "TCP");
+
+        if (client.client_id == first_me.client_id) {
+            found_first = true;
+            EXPECT_EQ(client.user_id, first_me.user_id);
+            EXPECT_EQ(static_cast<std::string>(client.address), static_cast<std::string>(first_me.address));
+            EXPECT_EQ(client.consumer_groups_count, first_me.consumer_groups_count);
+        }
+
+        if (client.client_id == second_me.client_id) {
+            found_second = true;
+            EXPECT_EQ(client.user_id, second_me.user_id);
+            EXPECT_EQ(static_cast<std::string>(client.address), static_cast<std::string>(second_me.address));
+            EXPECT_EQ(client.consumer_groups_count, second_me.consumer_groups_count);
+        }
+    }
+
+    EXPECT_TRUE(found_first);
+    EXPECT_TRUE(found_second);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsIsStableAcrossBackToBackCalls) {
+    RecordProperty("description", "Returns stable client lists across back-to-back get_clients calls.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    rust::Vec<iggy::ffi::ClientInfo> first_clients;
+    rust::Vec<iggy::ffi::ClientInfo> second_clients;
+    ASSERT_NO_THROW({
+        first_clients  = first_client->get_clients();
+        second_clients = first_client->get_clients();
+    });
+
+    ASSERT_EQ(second_clients.size(), first_clients.size());
+    for (const auto &first_entry : first_clients) {
+        bool found_match = false;
+        for (const auto &second_entry : second_clients) {
+            if (second_entry.client_id != first_entry.client_id) {
+                continue;
+            }
+
+            found_match = true;
+            EXPECT_EQ(second_entry.user_id, first_entry.user_id);
+            EXPECT_EQ(static_cast<std::string>(second_entry.address), static_cast<std::string>(first_entry.address));
+            EXPECT_EQ(static_cast<std::string>(second_entry.transport),
+                      static_cast<std::string>(first_entry.transport));
+            EXPECT_EQ(second_entry.consumer_groups_count, first_entry.consumer_groups_count);
+            break;
+        }
+        EXPECT_TRUE(found_match);
+    }
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsMatchesGetClientForReturnedIds) {
+    RecordProperty("description", "Returns list entries that agree with get_client for each returned client id.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    rust::Vec<iggy::ffi::ClientInfo> clients;
+    ASSERT_NO_THROW({ clients = first_client->get_clients(); });
+    ASSERT_GE(clients.size(), 2u);
+
+    for (const auto &client : clients) {
+        SCOPED_TRACE(client.client_id);
+        iggy::ffi::ClientInfoDetails details{};
+        ASSERT_NO_THROW({ details = first_client->get_client(client.client_id); });
+
+        EXPECT_EQ(details.client_id, client.client_id);
+        EXPECT_EQ(details.user_id, client.user_id);
+        EXPECT_EQ(static_cast<std::string>(details.address), static_cast<std::string>(client.address));
+        EXPECT_EQ(static_cast<std::string>(details.transport), static_cast<std::string>(client.transport));
+        EXPECT_EQ(details.consumer_groups_count, client.consumer_groups_count);
+    }
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsReflectsAdditionalSession) {
+    RecordProperty("description", "Reflects a newly added authenticated session in subsequent get_clients results.");
+    iggy::ffi::Client *first_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+
+    rust::Vec<iggy::ffi::ClientInfo> clients_before;
+    ASSERT_NO_THROW({ clients_before = first_client->get_clients(); });
+
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails second_me{};
+    rust::Vec<iggy::ffi::ClientInfo> clients_after;
+    ASSERT_NO_THROW({
+        second_me     = second_client->get_me();
+        clients_after = first_client->get_clients();
+    });
+
+    bool found_before = false;
+    for (const auto &client : clients_before) {
+        if (client.client_id == second_me.client_id) {
+            found_before = true;
+            break;
+        }
+    }
+    EXPECT_FALSE(found_before);
+
+    bool found_after = false;
+    for (const auto &client : clients_after) {
+        if (client.client_id != second_me.client_id) {
+            continue;
+        }
+
+        found_after = true;
+        EXPECT_EQ(client.user_id, second_me.user_id);
+        EXPECT_EQ(static_cast<std::string>(client.address), static_cast<std::string>(second_me.address));
+        EXPECT_EQ(static_cast<std::string>(client.transport), "TCP");
+        EXPECT_EQ(client.consumer_groups_count, second_me.consumer_groups_count);
+        break;
+    }
+    EXPECT_TRUE(found_after);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, PingSucceedsForNewConnection) {
+    RecordProperty("description", "Successfully pings the server from a fresh unauthenticated client session.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_NO_THROW(client->ping());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, HeartbeatIntervalReturnsDefaultValueForNewConnection) {
+    RecordProperty("description",
+                   "Returns the default heartbeat interval in microseconds for a fresh unauthenticated client.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    const auto heartbeat_interval = client->heartbeat_interval();
+    EXPECT_EQ(heartbeat_interval, 5'000'000u);

Review Comment:
   magic `5'000'000u` (and `10'000'000u` at L757). define `constexpr std::uint64_t kDefaultHeartbeatMicros = 5'000'000ull;` or derive from `IggyDuration::from("5s").as_micros()`.



##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -149,3 +153,671 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add a test to create some streams, topics, partitions, and segments, send messages, and create
+// consumer groups and verify it.
+TEST(LowLevelE2E_Client, GetStatsReturnsServerStats) {
+    RecordProperty("description",
+                   "Returns empty resource counts first, then reflects aggregated streams, topics, partitions, "
+                   "consumer groups, and clients.");
+    const std::string first_stream_name                 = "cpp-get-stats-stream-1";
+    const std::string second_stream_name                = "cpp-get-stats-stream-2";
+    const std::string first_topic_name                  = "cpp-get-stats-topic-1";
+    const std::string second_topic_name                 = "cpp-get-stats-topic-2";
+    const std::string third_topic_name                  = "cpp-get-stats-topic-3";
+    const std::string first_group_name                  = "cpp-get-stats-group-1";
+    const std::string second_group_name                 = "cpp-get-stats-group-2";
+    const std::string third_group_name                  = "cpp-get-stats-group-3";
+    constexpr std::uint32_t additional_partitions_count = 2;
+    iggy::ffi::Client *client                           = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Client *second_client = nullptr;
+    iggy::ffi::Client *third_client  = nullptr;
+
+    iggy::ffi::Stats empty_stats{};
+    ASSERT_NO_THROW({
+        empty_stats = client->get_stats();
+        EXPECT_NE(empty_stats.process_id, 0u);
+        EXPECT_GT(empty_stats.threads_count, 0u);
+        EXPECT_GT(empty_stats.total_memory, 0u);
+        EXPECT_GE(empty_stats.available_memory, 0u);
+        EXPECT_GE(empty_stats.total_disk_space, empty_stats.free_disk_space);
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.hostname).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_name).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.kernel_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.iggy_server_version).empty());
+        EXPECT_TRUE(empty_stats.iggy_server_semver == std::numeric_limits<std::uint32_t>::max() ||
+                    empty_stats.iggy_server_semver > 0u);
+        EXPECT_GE(empty_stats.cache_metrics.size(), 0u);
+        EXPECT_EQ(empty_stats.streams_count, 0u);
+        EXPECT_EQ(empty_stats.topics_count, 0u);
+        EXPECT_EQ(empty_stats.partitions_count, 0u);
+        EXPECT_EQ(empty_stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(client->create_stream(first_stream_name));
+    ASSERT_NO_THROW(client->create_stream(second_stream_name));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), first_topic_name, 1, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), second_topic_name, 2, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(second_stream_name), third_topic_name, 3, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_partitions(make_string_identifier(first_stream_name),
+                                              make_string_identifier(first_topic_name), additional_partitions_count));
+    const auto first_group  = client->create_consumer_group(make_string_identifier(first_stream_name),
+                                                            make_string_identifier(first_topic_name), first_group_name);
+    const auto second_group = client->create_consumer_group(
+        make_string_identifier(first_stream_name), make_string_identifier(second_topic_name), second_group_name);
+    const auto third_group = client->create_consumer_group(make_string_identifier(second_stream_name),
+                                                           make_string_identifier(third_topic_name), third_group_name);
+
+    ASSERT_NO_THROW({ second_client = login_to_server(); });
+    ASSERT_NE(second_client, nullptr);
+    ASSERT_NO_THROW({ third_client = login_to_server(); });
+    ASSERT_NE(third_client, nullptr);
+
+    const auto first_stream_details           = client->get_stream(make_string_identifier(first_stream_name));
+    const auto second_stream_details          = client->get_stream(make_string_identifier(second_stream_name));
+    const std::uint32_t expected_topics_count = first_stream_details.topics_count + second_stream_details.topics_count;
+    std::uint32_t first_topic_partitions      = 0;
+    std::uint32_t second_topic_partitions     = 0;
+    std::uint32_t third_topic_partitions      = 0;
+    for (const auto &topic : first_stream_details.topics) {
+        if (topic.name == first_topic_name) {
+            first_topic_partitions = topic.partitions_count;
+        }
+        if (topic.name == second_topic_name) {
+            second_topic_partitions = topic.partitions_count;
+        }
+    }
+    for (const auto &topic : second_stream_details.topics) {
+        if (topic.name == third_topic_name) {
+            third_topic_partitions = topic.partitions_count;
+        }
+    }
+    const std::uint32_t expected_partitions_count =
+        first_topic_partitions + second_topic_partitions + third_topic_partitions;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 2u);
+        EXPECT_EQ(stats.topics_count, expected_topics_count);
+        EXPECT_EQ(stats.partitions_count, expected_partitions_count);
+        EXPECT_EQ(stats.segments_count, expected_partitions_count);
+        EXPECT_EQ(stats.consumer_groups_count, 3u);
+        EXPECT_EQ(stats.clients_count, empty_stats.clients_count + 2u);
+        EXPECT_EQ(first_group.partitions_count, first_topic_partitions);
+        EXPECT_EQ(second_group.partitions_count, second_topic_partitions);
+        EXPECT_EQ(third_group.partitions_count, third_topic_partitions);
+    });
+
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(second_stream_name)));
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(first_stream_name)));
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(third_client));
+    third_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 0u);
+        EXPECT_EQ(stats.topics_count, 0u);
+        EXPECT_EQ(stats.partitions_count, 0u);
+        EXPECT_EQ(stats.segments_count, 0u);
+        EXPECT_EQ(stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetStatsIsStableAcrossBackToBackCalls) {
+    RecordProperty(
+        "description",
+        "Returns sane invariant fields across back-to-back get_stats calls on an idle authenticated client.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Stats first_stats{};
+    iggy::ffi::Stats second_stats{};
+    ASSERT_NO_THROW({
+        first_stats  = client->get_stats();
+        second_stats = client->get_stats();
+    });
+
+    EXPECT_NE(first_stats.process_id, 0u);
+    EXPECT_NE(second_stats.process_id, 0u);
+    EXPECT_EQ(second_stats.process_id, first_stats.process_id);
+    EXPECT_GT(first_stats.threads_count, 0u);
+    EXPECT_GT(second_stats.threads_count, 0u);
+    EXPECT_GT(first_stats.total_memory, 0u);
+    EXPECT_GT(second_stats.total_memory, 0u);
+    EXPECT_FALSE(static_cast<std::string>(first_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.iggy_server_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.iggy_server_version).empty());
+    EXPECT_EQ(static_cast<std::string>(second_stats.hostname), static_cast<std::string>(first_stats.hostname));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_name), static_cast<std::string>(first_stats.os_name));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_version), static_cast<std::string>(first_stats.os_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.kernel_version),
+              static_cast<std::string>(first_stats.kernel_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.iggy_server_version),
+              static_cast<std::string>(first_stats.iggy_server_version));
+    EXPECT_EQ(second_stats.iggy_server_semver, first_stats.iggy_server_semver);
+    EXPECT_EQ(second_stats.clients_count, first_stats.clients_count);
+    EXPECT_EQ(second_stats.streams_count, first_stats.streams_count);
+    EXPECT_EQ(second_stats.topics_count, first_stats.topics_count);
+    EXPECT_EQ(second_stats.partitions_count, first_stats.partitions_count);
+    EXPECT_EQ(second_stats.consumer_groups_count, first_stats.consumer_groups_count);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_me before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_me(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_me(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add additional validation for get_me after merging join_consumer_group PR.
+TEST(LowLevelE2E_Client, GetMeReturnsCurrentClientDetails) {
+    RecordProperty("description", "Returns the current authenticated client details.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_NO_THROW({
+        const auto me = client->get_me();
+        EXPECT_NE(me.client_id, 0u);
+        EXPECT_NE(me.user_id, std::numeric_limits<std::uint32_t>::max());
+        EXPECT_FALSE(static_cast<std::string>(me.address).empty());
+        EXPECT_EQ(static_cast<std::string>(me.transport), "TCP");
+        EXPECT_EQ(me.consumer_groups_count, 0u);
+        EXPECT_TRUE(me.consumer_groups.empty());
+    });
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeIsStableAcrossBackToBackCalls) {
+    RecordProperty("description", "Returns stable current-client details across back-to-back get_me calls.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({
+        first_me  = client->get_me();
+        second_me = client->get_me();
+    });
+
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_EQ(second_me.client_id, first_me.client_id);
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(second_me.address), static_cast<std::string>(first_me.address));
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), static_cast<std::string>(first_me.transport));
+    EXPECT_EQ(second_me.consumer_groups_count, first_me.consumer_groups_count);
+    EXPECT_EQ(second_me.consumer_groups.size(), first_me.consumer_groups.size());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeReturnsDistinctClientIdsForDifferentSessions) {
+    RecordProperty(
+        "description",
+        "Returns different client ids for separate authenticated sessions while keeping the same user identity.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({
+        first_me  = first_client->get_me();
+        second_me = second_client->get_me();
+    });
+
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_NE(second_me.client_id, 0u);
+    EXPECT_NE(second_me.client_id, first_me.client_id);
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeReturnsValidDetailsAfterReconnect) {
+    RecordProperty("description",
+                   "Returns valid current-client details after reconnecting with a fresh authenticated session.");
+    iggy::ffi::Client *first_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    ASSERT_NO_THROW({ first_me = first_client->get_me(); });
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_NE(first_me.user_id, std::numeric_limits<std::uint32_t>::max());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({ second_me = second_client->get_me(); });
+    EXPECT_NE(second_me.client_id, 0u);
+    EXPECT_NE(second_me.user_id, std::numeric_limits<std::uint32_t>::max());
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), static_cast<std::string>(first_me.transport));
+    EXPECT_FALSE(static_cast<std::string>(second_me.address).empty());
+    EXPECT_EQ(second_me.consumer_groups_count, 0u);
+    EXPECT_TRUE(second_me.consumer_groups.empty());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_client before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_client(1), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_client(1), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientWithWrongClientIdThrows) {
+    RecordProperty("description", "Rejects querying invalid or non-existent client ids.");
+    const std::uint32_t wrong_client_ids[] = {0u, std::numeric_limits<std::uint32_t>::max()};
+    iggy::ffi::Client *client              = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    for (const std::uint32_t wrong_client_id : wrong_client_ids) {
+        SCOPED_TRACE(wrong_client_id);
+        ASSERT_THROW(client->get_client(wrong_client_id), std::exception);
+    }
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientReturnsDetailsForMatchingClientId) {
+    RecordProperty("description", "Returns current client details when querying with the authenticated client id.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::ClientInfoDetails current_client{};
+    iggy::ffi::ClientInfoDetails looked_up_client{};
+    ASSERT_NO_THROW({
+        current_client   = client->get_me();
+        looked_up_client = client->get_client(current_client.client_id);
+    });
+
+    EXPECT_NE(current_client.client_id, 0u);
+    EXPECT_EQ(looked_up_client.client_id, current_client.client_id);
+    EXPECT_EQ(looked_up_client.user_id, current_client.user_id);
+    EXPECT_EQ(static_cast<std::string>(looked_up_client.address), static_cast<std::string>(current_client.address));
+    EXPECT_EQ(static_cast<std::string>(looked_up_client.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(looked_up_client.transport), static_cast<std::string>(current_client.transport));
+    EXPECT_EQ(looked_up_client.consumer_groups_count, current_client.consumer_groups_count);
+    EXPECT_EQ(looked_up_client.consumer_groups.size(), current_client.consumer_groups.size());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientIsStableAcrossBackToBackCalls) {
+    RecordProperty("description", "Returns stable client details across back-to-back get_client calls.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::ClientInfoDetails current_client{};
+    iggy::ffi::ClientInfoDetails first_lookup{};
+    iggy::ffi::ClientInfoDetails second_lookup{};
+    ASSERT_NO_THROW({
+        current_client = client->get_me();
+        first_lookup   = client->get_client(current_client.client_id);
+        second_lookup  = client->get_client(current_client.client_id);
+    });
+
+    EXPECT_NE(current_client.client_id, 0u);
+    EXPECT_EQ(first_lookup.client_id, current_client.client_id);
+    EXPECT_EQ(second_lookup.client_id, first_lookup.client_id);
+    EXPECT_EQ(second_lookup.user_id, first_lookup.user_id);
+    EXPECT_EQ(static_cast<std::string>(second_lookup.address), static_cast<std::string>(first_lookup.address));
+    EXPECT_EQ(static_cast<std::string>(first_lookup.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_lookup.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_lookup.transport), static_cast<std::string>(first_lookup.transport));
+    EXPECT_EQ(second_lookup.consumer_groups_count, first_lookup.consumer_groups_count);
+    EXPECT_EQ(second_lookup.consumer_groups.size(), first_lookup.consumer_groups.size());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_clients before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_clients(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_clients(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsReturnsActiveClientSessions) {
+    RecordProperty("description", "Returns the currently active authenticated client sessions.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails second_me{};
+    rust::Vec<iggy::ffi::ClientInfo> clients;
+    ASSERT_NO_THROW({
+        first_me  = first_client->get_me();
+        second_me = second_client->get_me();
+        clients   = first_client->get_clients();
+    });
+
+    ASSERT_GE(clients.size(), 2u);
+
+    bool found_first  = false;
+    bool found_second = false;
+    for (const auto &client : clients) {
+        EXPECT_NE(client.client_id, 0u);
+        EXPECT_EQ(static_cast<std::string>(client.transport), "TCP");
+
+        if (client.client_id == first_me.client_id) {
+            found_first = true;
+            EXPECT_EQ(client.user_id, first_me.user_id);
+            EXPECT_EQ(static_cast<std::string>(client.address), static_cast<std::string>(first_me.address));
+            EXPECT_EQ(client.consumer_groups_count, first_me.consumer_groups_count);
+        }
+
+        if (client.client_id == second_me.client_id) {
+            found_second = true;
+            EXPECT_EQ(client.user_id, second_me.user_id);
+            EXPECT_EQ(static_cast<std::string>(client.address), static_cast<std::string>(second_me.address));
+            EXPECT_EQ(client.consumer_groups_count, second_me.consumer_groups_count);
+        }
+    }
+
+    EXPECT_TRUE(found_first);
+    EXPECT_TRUE(found_second);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsIsStableAcrossBackToBackCalls) {
+    RecordProperty("description", "Returns stable client lists across back-to-back get_clients calls.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    rust::Vec<iggy::ffi::ClientInfo> first_clients;
+    rust::Vec<iggy::ffi::ClientInfo> second_clients;
+    ASSERT_NO_THROW({
+        first_clients  = first_client->get_clients();
+        second_clients = first_client->get_clients();
+    });
+
+    ASSERT_EQ(second_clients.size(), first_clients.size());
+    for (const auto &first_entry : first_clients) {
+        bool found_match = false;
+        for (const auto &second_entry : second_clients) {
+            if (second_entry.client_id != first_entry.client_id) {
+                continue;
+            }
+
+            found_match = true;
+            EXPECT_EQ(second_entry.user_id, first_entry.user_id);
+            EXPECT_EQ(static_cast<std::string>(second_entry.address), static_cast<std::string>(first_entry.address));
+            EXPECT_EQ(static_cast<std::string>(second_entry.transport),
+                      static_cast<std::string>(first_entry.transport));
+            EXPECT_EQ(second_entry.consumer_groups_count, first_entry.consumer_groups_count);
+            break;
+        }
+        EXPECT_TRUE(found_match);
+    }
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsMatchesGetClientForReturnedIds) {
+    RecordProperty("description", "Returns list entries that agree with get_client for each returned client id.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    rust::Vec<iggy::ffi::ClientInfo> clients;
+    ASSERT_NO_THROW({ clients = first_client->get_clients(); });
+    ASSERT_GE(clients.size(), 2u);
+
+    for (const auto &client : clients) {
+        SCOPED_TRACE(client.client_id);
+        iggy::ffi::ClientInfoDetails details{};
+        ASSERT_NO_THROW({ details = first_client->get_client(client.client_id); });
+
+        EXPECT_EQ(details.client_id, client.client_id);
+        EXPECT_EQ(details.user_id, client.user_id);
+        EXPECT_EQ(static_cast<std::string>(details.address), static_cast<std::string>(client.address));
+        EXPECT_EQ(static_cast<std::string>(details.transport), static_cast<std::string>(client.transport));
+        EXPECT_EQ(details.consumer_groups_count, client.consumer_groups_count);
+    }
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsReflectsAdditionalSession) {
+    RecordProperty("description", "Reflects a newly added authenticated session in subsequent get_clients results.");
+    iggy::ffi::Client *first_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+
+    rust::Vec<iggy::ffi::ClientInfo> clients_before;
+    ASSERT_NO_THROW({ clients_before = first_client->get_clients(); });
+
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails second_me{};
+    rust::Vec<iggy::ffi::ClientInfo> clients_after;
+    ASSERT_NO_THROW({
+        second_me     = second_client->get_me();
+        clients_after = first_client->get_clients();
+    });
+
+    bool found_before = false;
+    for (const auto &client : clients_before) {
+        if (client.client_id == second_me.client_id) {
+            found_before = true;
+            break;
+        }
+    }
+    EXPECT_FALSE(found_before);
+
+    bool found_after = false;
+    for (const auto &client : clients_after) {
+        if (client.client_id != second_me.client_id) {
+            continue;
+        }
+
+        found_after = true;
+        EXPECT_EQ(client.user_id, second_me.user_id);
+        EXPECT_EQ(static_cast<std::string>(client.address), static_cast<std::string>(second_me.address));
+        EXPECT_EQ(static_cast<std::string>(client.transport), "TCP");
+        EXPECT_EQ(client.consumer_groups_count, second_me.consumer_groups_count);
+        break;
+    }
+    EXPECT_TRUE(found_after);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, PingSucceedsForNewConnection) {
+    RecordProperty("description", "Successfully pings the server from a fresh unauthenticated client session.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_NO_THROW(client->ping());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, HeartbeatIntervalReturnsDefaultValueForNewConnection) {
+    RecordProperty("description",
+                   "Returns the default heartbeat interval in microseconds for a fresh unauthenticated client.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    const auto heartbeat_interval = client->heartbeat_interval();
+    EXPECT_EQ(heartbeat_interval, 5'000'000u);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, HeartbeatIntervalReturnsConfiguredValueFromConnectionString) {
+    RecordProperty("description",
+                   "Returns the configured heartbeat interval in microseconds from the connection string.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection("iggy://iggy:iggy@127.0.0.1:8090?heartbeat_interval=10s"); });
+    ASSERT_NE(client, nullptr);
+
+    const auto heartbeat_interval = client->heartbeat_interval();
+    EXPECT_EQ(heartbeat_interval, 10'000'000u);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, SnapshotBeforeLoginThrows) {
+    RecordProperty("description", "Rejects snapshot before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    rust::Vec<rust::String> snapshot_types_before_connect;

Review Comment:
   `rust::Vec<rust::String> snapshot_types; snapshot_types.push_back(...)` repeats at L774, L787, L802, L816. add a `make_snapshot_types({"test"})` helper in `tests/common/test_helpers.hpp`.



##########
foreign/cpp/src/client.rs:
##########
@@ -378,6 +479,98 @@ impl Client {
             Ok(())
         })
     }
+
+    pub fn get_stats(&self) -> Result<ffi::Stats, String> {
+        RUNTIME.block_on(async {
+            let stats = self
+                .inner
+                .get_stats()
+                .await
+                .map_err(|error| format!("Could not get stats: {error}"))?;
+            Ok(ffi::Stats::from(stats))
+        })
+    }
+
+    pub fn get_me(&self) -> Result<ffi::ClientInfoDetails, String> {
+        RUNTIME.block_on(async {
+            let client = self
+                .inner
+                .get_me()
+                .await
+                .map_err(|error| format!("Could not get current client info: {error}"))?;
+            Ok(ffi::ClientInfoDetails::from(client))
+        })
+    }
+
+    pub fn get_client(&self, client_id: u32) -> Result<ffi::ClientInfoDetails, String> {
+        RUNTIME.block_on(async {
+            let client = self
+                .inner
+                .get_client(client_id)
+                .await
+                .map_err(|error| format!("Could not get client '{client_id}': {error}"))?;
+            ffi::ClientInfoDetails::try_from(client)
+                .map_err(|error| format!("Could not get client '{client_id}': {error}"))
+        })
+    }
+
+    pub fn get_clients(&self) -> Result<Vec<ffi::ClientInfo>, String> {
+        RUNTIME.block_on(async {
+            let clients = self
+                .inner
+                .get_clients()
+                .await
+                .map_err(|error| format!("Could not get clients: {error}"))?;
+            Ok(clients.into_iter().map(ffi::ClientInfo::from).collect())
+        })
+    }
+
+    pub fn ping(&self) -> Result<(), String> {
+        RUNTIME.block_on(async {
+            self.inner
+                .ping()
+                .await
+                .map_err(|error| format!("Could not ping server: {error}"))?;
+            Ok(())
+        })
+    }
+
+    pub fn heartbeat_interval(&self) -> u64 {
+        RUNTIME.block_on(async { self.inner.heartbeat_interval().await.as_micros() })
+    }
+
+    pub fn snapshot(
+        &self,
+        snapshot_compression: String,
+        snapshot_types: Vec<String>,
+    ) -> Result<Vec<u8>, String> {
+        let rust_compression = match snapshot_compression.trim() {
+            "" => RustSnapshotCompression::default(),
+            value => RustSnapshotCompression::from_str(value).map_err(|error| {
+                format!("Could not capture snapshot: invalid compression '{value}': {error}")
+            })?,
+        };
+        let rust_snapshot_types = snapshot_types
+            .into_iter()
+            .map(|snapshot_type| {
+                RustSystemSnapshotType::from_str(&snapshot_type).map_err(|error| {
+                    format!(
+                        "Could not capture snapshot: invalid snapshot type '{}': {}",

Review Comment:
   format string mixes positional `'{}': {}` with `snapshot_type, error` while the sibling at L550 uses named captures. use `'{snapshot_type}': {error}` for consistency. `cargo clippy -- -W clippy::uninlined_format_args` would catch it.



##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -149,3 +153,671 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add a test to create some streams, topics, partitions, and segments, send messages, and create
+// consumer groups and verify it.
+TEST(LowLevelE2E_Client, GetStatsReturnsServerStats) {
+    RecordProperty("description",
+                   "Returns empty resource counts first, then reflects aggregated streams, topics, partitions, "
+                   "consumer groups, and clients.");
+    const std::string first_stream_name                 = "cpp-get-stats-stream-1";
+    const std::string second_stream_name                = "cpp-get-stats-stream-2";
+    const std::string first_topic_name                  = "cpp-get-stats-topic-1";
+    const std::string second_topic_name                 = "cpp-get-stats-topic-2";
+    const std::string third_topic_name                  = "cpp-get-stats-topic-3";
+    const std::string first_group_name                  = "cpp-get-stats-group-1";
+    const std::string second_group_name                 = "cpp-get-stats-group-2";
+    const std::string third_group_name                  = "cpp-get-stats-group-3";
+    constexpr std::uint32_t additional_partitions_count = 2;
+    iggy::ffi::Client *client                           = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Client *second_client = nullptr;
+    iggy::ffi::Client *third_client  = nullptr;
+
+    iggy::ffi::Stats empty_stats{};
+    ASSERT_NO_THROW({
+        empty_stats = client->get_stats();
+        EXPECT_NE(empty_stats.process_id, 0u);
+        EXPECT_GT(empty_stats.threads_count, 0u);
+        EXPECT_GT(empty_stats.total_memory, 0u);
+        EXPECT_GE(empty_stats.available_memory, 0u);
+        EXPECT_GE(empty_stats.total_disk_space, empty_stats.free_disk_space);
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.hostname).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_name).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.kernel_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.iggy_server_version).empty());
+        EXPECT_TRUE(empty_stats.iggy_server_semver == std::numeric_limits<std::uint32_t>::max() ||
+                    empty_stats.iggy_server_semver > 0u);
+        EXPECT_GE(empty_stats.cache_metrics.size(), 0u);
+        EXPECT_EQ(empty_stats.streams_count, 0u);
+        EXPECT_EQ(empty_stats.topics_count, 0u);
+        EXPECT_EQ(empty_stats.partitions_count, 0u);
+        EXPECT_EQ(empty_stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(client->create_stream(first_stream_name));
+    ASSERT_NO_THROW(client->create_stream(second_stream_name));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), first_topic_name, 1, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), second_topic_name, 2, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(second_stream_name), third_topic_name, 3, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_partitions(make_string_identifier(first_stream_name),
+                                              make_string_identifier(first_topic_name), additional_partitions_count));
+    const auto first_group  = client->create_consumer_group(make_string_identifier(first_stream_name),
+                                                            make_string_identifier(first_topic_name), first_group_name);
+    const auto second_group = client->create_consumer_group(
+        make_string_identifier(first_stream_name), make_string_identifier(second_topic_name), second_group_name);
+    const auto third_group = client->create_consumer_group(make_string_identifier(second_stream_name),
+                                                           make_string_identifier(third_topic_name), third_group_name);
+
+    ASSERT_NO_THROW({ second_client = login_to_server(); });
+    ASSERT_NE(second_client, nullptr);
+    ASSERT_NO_THROW({ third_client = login_to_server(); });
+    ASSERT_NE(third_client, nullptr);
+
+    const auto first_stream_details           = client->get_stream(make_string_identifier(first_stream_name));
+    const auto second_stream_details          = client->get_stream(make_string_identifier(second_stream_name));
+    const std::uint32_t expected_topics_count = first_stream_details.topics_count + second_stream_details.topics_count;
+    std::uint32_t first_topic_partitions      = 0;
+    std::uint32_t second_topic_partitions     = 0;
+    std::uint32_t third_topic_partitions      = 0;
+    for (const auto &topic : first_stream_details.topics) {
+        if (topic.name == first_topic_name) {
+            first_topic_partitions = topic.partitions_count;
+        }
+        if (topic.name == second_topic_name) {
+            second_topic_partitions = topic.partitions_count;
+        }
+    }
+    for (const auto &topic : second_stream_details.topics) {
+        if (topic.name == third_topic_name) {
+            third_topic_partitions = topic.partitions_count;
+        }
+    }
+    const std::uint32_t expected_partitions_count =
+        first_topic_partitions + second_topic_partitions + third_topic_partitions;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 2u);
+        EXPECT_EQ(stats.topics_count, expected_topics_count);
+        EXPECT_EQ(stats.partitions_count, expected_partitions_count);
+        EXPECT_EQ(stats.segments_count, expected_partitions_count);
+        EXPECT_EQ(stats.consumer_groups_count, 3u);
+        EXPECT_EQ(stats.clients_count, empty_stats.clients_count + 2u);
+        EXPECT_EQ(first_group.partitions_count, first_topic_partitions);
+        EXPECT_EQ(second_group.partitions_count, second_topic_partitions);
+        EXPECT_EQ(third_group.partitions_count, third_topic_partitions);
+    });
+
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(second_stream_name)));
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(first_stream_name)));
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(third_client));
+    third_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 0u);
+        EXPECT_EQ(stats.topics_count, 0u);
+        EXPECT_EQ(stats.partitions_count, 0u);
+        EXPECT_EQ(stats.segments_count, 0u);
+        EXPECT_EQ(stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetStatsIsStableAcrossBackToBackCalls) {
+    RecordProperty(
+        "description",
+        "Returns sane invariant fields across back-to-back get_stats calls on an idle authenticated client.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Stats first_stats{};
+    iggy::ffi::Stats second_stats{};
+    ASSERT_NO_THROW({
+        first_stats  = client->get_stats();
+        second_stats = client->get_stats();
+    });
+
+    EXPECT_NE(first_stats.process_id, 0u);
+    EXPECT_NE(second_stats.process_id, 0u);
+    EXPECT_EQ(second_stats.process_id, first_stats.process_id);
+    EXPECT_GT(first_stats.threads_count, 0u);
+    EXPECT_GT(second_stats.threads_count, 0u);
+    EXPECT_GT(first_stats.total_memory, 0u);
+    EXPECT_GT(second_stats.total_memory, 0u);
+    EXPECT_FALSE(static_cast<std::string>(first_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.iggy_server_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.iggy_server_version).empty());
+    EXPECT_EQ(static_cast<std::string>(second_stats.hostname), static_cast<std::string>(first_stats.hostname));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_name), static_cast<std::string>(first_stats.os_name));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_version), static_cast<std::string>(first_stats.os_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.kernel_version),
+              static_cast<std::string>(first_stats.kernel_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.iggy_server_version),
+              static_cast<std::string>(first_stats.iggy_server_version));
+    EXPECT_EQ(second_stats.iggy_server_semver, first_stats.iggy_server_semver);
+    EXPECT_EQ(second_stats.clients_count, first_stats.clients_count);
+    EXPECT_EQ(second_stats.streams_count, first_stats.streams_count);
+    EXPECT_EQ(second_stats.topics_count, first_stats.topics_count);
+    EXPECT_EQ(second_stats.partitions_count, first_stats.partitions_count);
+    EXPECT_EQ(second_stats.consumer_groups_count, first_stats.consumer_groups_count);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_me before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_me(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_me(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add additional validation for get_me after merging join_consumer_group PR.

Review Comment:
   same `TODO(slbotbm-this PR)` marker. resolve or strip.



##########
foreign/cpp/include/iggy.hpp:
##########
@@ -45,6 +45,42 @@ class CompressionAlgorithm final {
     std::string algorithm_;
 };
 
+class SnapshotCompression final {
+  public:
+    static SnapshotCompression stored() { return SnapshotCompression("stored"); }

Review Comment:
   factory enforces a fixed enumeration but the FFI signature accepts `String`, which is then re-parsed in `RustSnapshotCompression::from_str` (client.rs:549). either pass an enum tag across FFI or document the parse-trip contract.



##########
foreign/cpp/src/identifier.rs:
##########
@@ -62,6 +62,7 @@ impl TryFrom<ffi::Identifier> for RustIdentifier {
     }
 }
 
+#[allow(clippy::wrong_self_convention)]

Review Comment:
   `#[allow(clippy::wrong_self_convention)]` silences a real signal. `from_string`/`from_numeric` take `&mut self` and mutate in place - the names lie about ownership. rename to `set_string`/`set_numeric` (update lib.rs:210-211 bridge + c++ callers) and drop the suppression.



##########
foreign/cpp/include/iggy.hpp:
##########
@@ -45,6 +45,42 @@ class CompressionAlgorithm final {
     std::string algorithm_;
 };
 
+class SnapshotCompression final {

Review Comment:
   `SnapshotCompression`/`SnapshotType` (and the existing `IdKind`/`CompressionAlgorithm`) are identical `std::string` wrapper-with-factory patterns. extract a `StringTag<Tag>` template/CRTP base.



##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -149,3 +153,671 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add a test to create some streams, topics, partitions, and segments, send messages, and create
+// consumer groups and verify it.
+TEST(LowLevelE2E_Client, GetStatsReturnsServerStats) {
+    RecordProperty("description",
+                   "Returns empty resource counts first, then reflects aggregated streams, topics, partitions, "
+                   "consumer groups, and clients.");
+    const std::string first_stream_name                 = "cpp-get-stats-stream-1";
+    const std::string second_stream_name                = "cpp-get-stats-stream-2";
+    const std::string first_topic_name                  = "cpp-get-stats-topic-1";
+    const std::string second_topic_name                 = "cpp-get-stats-topic-2";
+    const std::string third_topic_name                  = "cpp-get-stats-topic-3";
+    const std::string first_group_name                  = "cpp-get-stats-group-1";
+    const std::string second_group_name                 = "cpp-get-stats-group-2";
+    const std::string third_group_name                  = "cpp-get-stats-group-3";
+    constexpr std::uint32_t additional_partitions_count = 2;
+    iggy::ffi::Client *client                           = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Client *second_client = nullptr;
+    iggy::ffi::Client *third_client  = nullptr;
+
+    iggy::ffi::Stats empty_stats{};
+    ASSERT_NO_THROW({
+        empty_stats = client->get_stats();
+        EXPECT_NE(empty_stats.process_id, 0u);
+        EXPECT_GT(empty_stats.threads_count, 0u);
+        EXPECT_GT(empty_stats.total_memory, 0u);
+        EXPECT_GE(empty_stats.available_memory, 0u);
+        EXPECT_GE(empty_stats.total_disk_space, empty_stats.free_disk_space);
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.hostname).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_name).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.kernel_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.iggy_server_version).empty());
+        EXPECT_TRUE(empty_stats.iggy_server_semver == std::numeric_limits<std::uint32_t>::max() ||
+                    empty_stats.iggy_server_semver > 0u);
+        EXPECT_GE(empty_stats.cache_metrics.size(), 0u);
+        EXPECT_EQ(empty_stats.streams_count, 0u);
+        EXPECT_EQ(empty_stats.topics_count, 0u);
+        EXPECT_EQ(empty_stats.partitions_count, 0u);
+        EXPECT_EQ(empty_stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(client->create_stream(first_stream_name));
+    ASSERT_NO_THROW(client->create_stream(second_stream_name));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), first_topic_name, 1, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), second_topic_name, 2, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(second_stream_name), third_topic_name, 3, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_partitions(make_string_identifier(first_stream_name),
+                                              make_string_identifier(first_topic_name), additional_partitions_count));
+    const auto first_group  = client->create_consumer_group(make_string_identifier(first_stream_name),
+                                                            make_string_identifier(first_topic_name), first_group_name);
+    const auto second_group = client->create_consumer_group(
+        make_string_identifier(first_stream_name), make_string_identifier(second_topic_name), second_group_name);
+    const auto third_group = client->create_consumer_group(make_string_identifier(second_stream_name),
+                                                           make_string_identifier(third_topic_name), third_group_name);
+
+    ASSERT_NO_THROW({ second_client = login_to_server(); });
+    ASSERT_NE(second_client, nullptr);
+    ASSERT_NO_THROW({ third_client = login_to_server(); });
+    ASSERT_NE(third_client, nullptr);
+
+    const auto first_stream_details           = client->get_stream(make_string_identifier(first_stream_name));
+    const auto second_stream_details          = client->get_stream(make_string_identifier(second_stream_name));
+    const std::uint32_t expected_topics_count = first_stream_details.topics_count + second_stream_details.topics_count;
+    std::uint32_t first_topic_partitions      = 0;
+    std::uint32_t second_topic_partitions     = 0;
+    std::uint32_t third_topic_partitions      = 0;
+    for (const auto &topic : first_stream_details.topics) {
+        if (topic.name == first_topic_name) {
+            first_topic_partitions = topic.partitions_count;
+        }
+        if (topic.name == second_topic_name) {
+            second_topic_partitions = topic.partitions_count;
+        }
+    }
+    for (const auto &topic : second_stream_details.topics) {
+        if (topic.name == third_topic_name) {
+            third_topic_partitions = topic.partitions_count;
+        }
+    }
+    const std::uint32_t expected_partitions_count =
+        first_topic_partitions + second_topic_partitions + third_topic_partitions;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 2u);
+        EXPECT_EQ(stats.topics_count, expected_topics_count);
+        EXPECT_EQ(stats.partitions_count, expected_partitions_count);
+        EXPECT_EQ(stats.segments_count, expected_partitions_count);
+        EXPECT_EQ(stats.consumer_groups_count, 3u);
+        EXPECT_EQ(stats.clients_count, empty_stats.clients_count + 2u);
+        EXPECT_EQ(first_group.partitions_count, first_topic_partitions);
+        EXPECT_EQ(second_group.partitions_count, second_topic_partitions);
+        EXPECT_EQ(third_group.partitions_count, third_topic_partitions);
+    });
+
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(second_stream_name)));
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(first_stream_name)));
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(third_client));
+    third_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 0u);
+        EXPECT_EQ(stats.topics_count, 0u);
+        EXPECT_EQ(stats.partitions_count, 0u);
+        EXPECT_EQ(stats.segments_count, 0u);
+        EXPECT_EQ(stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetStatsIsStableAcrossBackToBackCalls) {
+    RecordProperty(
+        "description",
+        "Returns sane invariant fields across back-to-back get_stats calls on an idle authenticated client.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Stats first_stats{};
+    iggy::ffi::Stats second_stats{};
+    ASSERT_NO_THROW({
+        first_stats  = client->get_stats();
+        second_stats = client->get_stats();
+    });
+
+    EXPECT_NE(first_stats.process_id, 0u);
+    EXPECT_NE(second_stats.process_id, 0u);
+    EXPECT_EQ(second_stats.process_id, first_stats.process_id);
+    EXPECT_GT(first_stats.threads_count, 0u);
+    EXPECT_GT(second_stats.threads_count, 0u);
+    EXPECT_GT(first_stats.total_memory, 0u);
+    EXPECT_GT(second_stats.total_memory, 0u);
+    EXPECT_FALSE(static_cast<std::string>(first_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.iggy_server_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.iggy_server_version).empty());
+    EXPECT_EQ(static_cast<std::string>(second_stats.hostname), static_cast<std::string>(first_stats.hostname));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_name), static_cast<std::string>(first_stats.os_name));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_version), static_cast<std::string>(first_stats.os_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.kernel_version),
+              static_cast<std::string>(first_stats.kernel_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.iggy_server_version),
+              static_cast<std::string>(first_stats.iggy_server_version));
+    EXPECT_EQ(second_stats.iggy_server_semver, first_stats.iggy_server_semver);
+    EXPECT_EQ(second_stats.clients_count, first_stats.clients_count);
+    EXPECT_EQ(second_stats.streams_count, first_stats.streams_count);
+    EXPECT_EQ(second_stats.topics_count, first_stats.topics_count);
+    EXPECT_EQ(second_stats.partitions_count, first_stats.partitions_count);
+    EXPECT_EQ(second_stats.consumer_groups_count, first_stats.consumer_groups_count);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_me before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_me(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_me(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add additional validation for get_me after merging join_consumer_group PR.
+TEST(LowLevelE2E_Client, GetMeReturnsCurrentClientDetails) {
+    RecordProperty("description", "Returns the current authenticated client details.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_NO_THROW({
+        const auto me = client->get_me();
+        EXPECT_NE(me.client_id, 0u);
+        EXPECT_NE(me.user_id, std::numeric_limits<std::uint32_t>::max());

Review Comment:
   `EXPECT_NE(me.user_id, max())` couples test correctness to the sentinel hack at client.rs:40. also at L438, L449. once the option/none plumbing lands these flip to `EXPECT_TRUE(me.has_user_id)`.



##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -149,3 +153,671 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add a test to create some streams, topics, partitions, and segments, send messages, and create
+// consumer groups and verify it.
+TEST(LowLevelE2E_Client, GetStatsReturnsServerStats) {
+    RecordProperty("description",
+                   "Returns empty resource counts first, then reflects aggregated streams, topics, partitions, "
+                   "consumer groups, and clients.");
+    const std::string first_stream_name                 = "cpp-get-stats-stream-1";
+    const std::string second_stream_name                = "cpp-get-stats-stream-2";
+    const std::string first_topic_name                  = "cpp-get-stats-topic-1";
+    const std::string second_topic_name                 = "cpp-get-stats-topic-2";
+    const std::string third_topic_name                  = "cpp-get-stats-topic-3";
+    const std::string first_group_name                  = "cpp-get-stats-group-1";
+    const std::string second_group_name                 = "cpp-get-stats-group-2";
+    const std::string third_group_name                  = "cpp-get-stats-group-3";
+    constexpr std::uint32_t additional_partitions_count = 2;
+    iggy::ffi::Client *client                           = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Client *second_client = nullptr;
+    iggy::ffi::Client *third_client  = nullptr;
+
+    iggy::ffi::Stats empty_stats{};
+    ASSERT_NO_THROW({
+        empty_stats = client->get_stats();
+        EXPECT_NE(empty_stats.process_id, 0u);
+        EXPECT_GT(empty_stats.threads_count, 0u);
+        EXPECT_GT(empty_stats.total_memory, 0u);
+        EXPECT_GE(empty_stats.available_memory, 0u);
+        EXPECT_GE(empty_stats.total_disk_space, empty_stats.free_disk_space);
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.hostname).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_name).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.kernel_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.iggy_server_version).empty());
+        EXPECT_TRUE(empty_stats.iggy_server_semver == std::numeric_limits<std::uint32_t>::max() ||
+                    empty_stats.iggy_server_semver > 0u);
+        EXPECT_GE(empty_stats.cache_metrics.size(), 0u);
+        EXPECT_EQ(empty_stats.streams_count, 0u);
+        EXPECT_EQ(empty_stats.topics_count, 0u);
+        EXPECT_EQ(empty_stats.partitions_count, 0u);
+        EXPECT_EQ(empty_stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(client->create_stream(first_stream_name));
+    ASSERT_NO_THROW(client->create_stream(second_stream_name));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), first_topic_name, 1, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), second_topic_name, 2, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(second_stream_name), third_topic_name, 3, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_partitions(make_string_identifier(first_stream_name),
+                                              make_string_identifier(first_topic_name), additional_partitions_count));
+    const auto first_group  = client->create_consumer_group(make_string_identifier(first_stream_name),
+                                                            make_string_identifier(first_topic_name), first_group_name);
+    const auto second_group = client->create_consumer_group(
+        make_string_identifier(first_stream_name), make_string_identifier(second_topic_name), second_group_name);
+    const auto third_group = client->create_consumer_group(make_string_identifier(second_stream_name),
+                                                           make_string_identifier(third_topic_name), third_group_name);
+
+    ASSERT_NO_THROW({ second_client = login_to_server(); });
+    ASSERT_NE(second_client, nullptr);
+    ASSERT_NO_THROW({ third_client = login_to_server(); });
+    ASSERT_NE(third_client, nullptr);
+
+    const auto first_stream_details           = client->get_stream(make_string_identifier(first_stream_name));
+    const auto second_stream_details          = client->get_stream(make_string_identifier(second_stream_name));
+    const std::uint32_t expected_topics_count = first_stream_details.topics_count + second_stream_details.topics_count;
+    std::uint32_t first_topic_partitions      = 0;
+    std::uint32_t second_topic_partitions     = 0;
+    std::uint32_t third_topic_partitions      = 0;
+    for (const auto &topic : first_stream_details.topics) {
+        if (topic.name == first_topic_name) {
+            first_topic_partitions = topic.partitions_count;
+        }
+        if (topic.name == second_topic_name) {
+            second_topic_partitions = topic.partitions_count;
+        }
+    }
+    for (const auto &topic : second_stream_details.topics) {
+        if (topic.name == third_topic_name) {
+            third_topic_partitions = topic.partitions_count;
+        }
+    }
+    const std::uint32_t expected_partitions_count =
+        first_topic_partitions + second_topic_partitions + third_topic_partitions;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 2u);
+        EXPECT_EQ(stats.topics_count, expected_topics_count);
+        EXPECT_EQ(stats.partitions_count, expected_partitions_count);
+        EXPECT_EQ(stats.segments_count, expected_partitions_count);
+        EXPECT_EQ(stats.consumer_groups_count, 3u);
+        EXPECT_EQ(stats.clients_count, empty_stats.clients_count + 2u);
+        EXPECT_EQ(first_group.partitions_count, first_topic_partitions);
+        EXPECT_EQ(second_group.partitions_count, second_topic_partitions);
+        EXPECT_EQ(third_group.partitions_count, third_topic_partitions);
+    });
+
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(second_stream_name)));
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(first_stream_name)));
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(third_client));
+    third_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 0u);
+        EXPECT_EQ(stats.topics_count, 0u);
+        EXPECT_EQ(stats.partitions_count, 0u);
+        EXPECT_EQ(stats.segments_count, 0u);
+        EXPECT_EQ(stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetStatsIsStableAcrossBackToBackCalls) {
+    RecordProperty(
+        "description",
+        "Returns sane invariant fields across back-to-back get_stats calls on an idle authenticated client.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Stats first_stats{};
+    iggy::ffi::Stats second_stats{};
+    ASSERT_NO_THROW({
+        first_stats  = client->get_stats();
+        second_stats = client->get_stats();
+    });
+
+    EXPECT_NE(first_stats.process_id, 0u);
+    EXPECT_NE(second_stats.process_id, 0u);
+    EXPECT_EQ(second_stats.process_id, first_stats.process_id);
+    EXPECT_GT(first_stats.threads_count, 0u);
+    EXPECT_GT(second_stats.threads_count, 0u);
+    EXPECT_GT(first_stats.total_memory, 0u);
+    EXPECT_GT(second_stats.total_memory, 0u);
+    EXPECT_FALSE(static_cast<std::string>(first_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.iggy_server_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.iggy_server_version).empty());
+    EXPECT_EQ(static_cast<std::string>(second_stats.hostname), static_cast<std::string>(first_stats.hostname));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_name), static_cast<std::string>(first_stats.os_name));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_version), static_cast<std::string>(first_stats.os_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.kernel_version),
+              static_cast<std::string>(first_stats.kernel_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.iggy_server_version),
+              static_cast<std::string>(first_stats.iggy_server_version));
+    EXPECT_EQ(second_stats.iggy_server_semver, first_stats.iggy_server_semver);
+    EXPECT_EQ(second_stats.clients_count, first_stats.clients_count);
+    EXPECT_EQ(second_stats.streams_count, first_stats.streams_count);
+    EXPECT_EQ(second_stats.topics_count, first_stats.topics_count);
+    EXPECT_EQ(second_stats.partitions_count, first_stats.partitions_count);
+    EXPECT_EQ(second_stats.consumer_groups_count, first_stats.consumer_groups_count);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_me before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_me(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_me(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add additional validation for get_me after merging join_consumer_group PR.
+TEST(LowLevelE2E_Client, GetMeReturnsCurrentClientDetails) {
+    RecordProperty("description", "Returns the current authenticated client details.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_NO_THROW({
+        const auto me = client->get_me();
+        EXPECT_NE(me.client_id, 0u);
+        EXPECT_NE(me.user_id, std::numeric_limits<std::uint32_t>::max());
+        EXPECT_FALSE(static_cast<std::string>(me.address).empty());
+        EXPECT_EQ(static_cast<std::string>(me.transport), "TCP");
+        EXPECT_EQ(me.consumer_groups_count, 0u);
+        EXPECT_TRUE(me.consumer_groups.empty());
+    });
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeIsStableAcrossBackToBackCalls) {
+    RecordProperty("description", "Returns stable current-client details across back-to-back get_me calls.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({
+        first_me  = client->get_me();
+        second_me = client->get_me();
+    });
+
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_EQ(second_me.client_id, first_me.client_id);
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(second_me.address), static_cast<std::string>(first_me.address));
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), static_cast<std::string>(first_me.transport));
+    EXPECT_EQ(second_me.consumer_groups_count, first_me.consumer_groups_count);
+    EXPECT_EQ(second_me.consumer_groups.size(), first_me.consumer_groups.size());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeReturnsDistinctClientIdsForDifferentSessions) {
+    RecordProperty(
+        "description",
+        "Returns different client ids for separate authenticated sessions while keeping the same user identity.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({
+        first_me  = first_client->get_me();
+        second_me = second_client->get_me();
+    });
+
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_NE(second_me.client_id, 0u);
+    EXPECT_NE(second_me.client_id, first_me.client_id);
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeReturnsValidDetailsAfterReconnect) {
+    RecordProperty("description",
+                   "Returns valid current-client details after reconnecting with a fresh authenticated session.");
+    iggy::ffi::Client *first_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    ASSERT_NO_THROW({ first_me = first_client->get_me(); });
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_NE(first_me.user_id, std::numeric_limits<std::uint32_t>::max());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({ second_me = second_client->get_me(); });
+    EXPECT_NE(second_me.client_id, 0u);
+    EXPECT_NE(second_me.user_id, std::numeric_limits<std::uint32_t>::max());
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), static_cast<std::string>(first_me.transport));
+    EXPECT_FALSE(static_cast<std::string>(second_me.address).empty());
+    EXPECT_EQ(second_me.consumer_groups_count, 0u);
+    EXPECT_TRUE(second_me.consumer_groups.empty());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_client before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_client(1), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_client(1), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientWithWrongClientIdThrows) {
+    RecordProperty("description", "Rejects querying invalid or non-existent client ids.");
+    const std::uint32_t wrong_client_ids[] = {0u, std::numeric_limits<std::uint32_t>::max()};
+    iggy::ffi::Client *client              = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    for (const std::uint32_t wrong_client_id : wrong_client_ids) {
+        SCOPED_TRACE(wrong_client_id);
+        ASSERT_THROW(client->get_client(wrong_client_id), std::exception);
+    }
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientReturnsDetailsForMatchingClientId) {
+    RecordProperty("description", "Returns current client details when querying with the authenticated client id.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::ClientInfoDetails current_client{};
+    iggy::ffi::ClientInfoDetails looked_up_client{};
+    ASSERT_NO_THROW({
+        current_client   = client->get_me();
+        looked_up_client = client->get_client(current_client.client_id);
+    });
+
+    EXPECT_NE(current_client.client_id, 0u);
+    EXPECT_EQ(looked_up_client.client_id, current_client.client_id);
+    EXPECT_EQ(looked_up_client.user_id, current_client.user_id);
+    EXPECT_EQ(static_cast<std::string>(looked_up_client.address), static_cast<std::string>(current_client.address));
+    EXPECT_EQ(static_cast<std::string>(looked_up_client.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(looked_up_client.transport), static_cast<std::string>(current_client.transport));
+    EXPECT_EQ(looked_up_client.consumer_groups_count, current_client.consumer_groups_count);
+    EXPECT_EQ(looked_up_client.consumer_groups.size(), current_client.consumer_groups.size());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientIsStableAcrossBackToBackCalls) {
+    RecordProperty("description", "Returns stable client details across back-to-back get_client calls.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::ClientInfoDetails current_client{};
+    iggy::ffi::ClientInfoDetails first_lookup{};
+    iggy::ffi::ClientInfoDetails second_lookup{};
+    ASSERT_NO_THROW({
+        current_client = client->get_me();
+        first_lookup   = client->get_client(current_client.client_id);
+        second_lookup  = client->get_client(current_client.client_id);
+    });
+
+    EXPECT_NE(current_client.client_id, 0u);
+    EXPECT_EQ(first_lookup.client_id, current_client.client_id);
+    EXPECT_EQ(second_lookup.client_id, first_lookup.client_id);
+    EXPECT_EQ(second_lookup.user_id, first_lookup.user_id);
+    EXPECT_EQ(static_cast<std::string>(second_lookup.address), static_cast<std::string>(first_lookup.address));
+    EXPECT_EQ(static_cast<std::string>(first_lookup.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_lookup.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_lookup.transport), static_cast<std::string>(first_lookup.transport));
+    EXPECT_EQ(second_lookup.consumer_groups_count, first_lookup.consumer_groups_count);
+    EXPECT_EQ(second_lookup.consumer_groups.size(), first_lookup.consumer_groups.size());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_clients before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_clients(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_clients(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsReturnsActiveClientSessions) {
+    RecordProperty("description", "Returns the currently active authenticated client sessions.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails second_me{};
+    rust::Vec<iggy::ffi::ClientInfo> clients;
+    ASSERT_NO_THROW({
+        first_me  = first_client->get_me();
+        second_me = second_client->get_me();
+        clients   = first_client->get_clients();
+    });
+
+    ASSERT_GE(clients.size(), 2u);
+
+    bool found_first  = false;
+    bool found_second = false;
+    for (const auto &client : clients) {
+        EXPECT_NE(client.client_id, 0u);
+        EXPECT_EQ(static_cast<std::string>(client.transport), "TCP");
+
+        if (client.client_id == first_me.client_id) {
+            found_first = true;
+            EXPECT_EQ(client.user_id, first_me.user_id);
+            EXPECT_EQ(static_cast<std::string>(client.address), static_cast<std::string>(first_me.address));
+            EXPECT_EQ(client.consumer_groups_count, first_me.consumer_groups_count);
+        }
+
+        if (client.client_id == second_me.client_id) {
+            found_second = true;
+            EXPECT_EQ(client.user_id, second_me.user_id);
+            EXPECT_EQ(static_cast<std::string>(client.address), static_cast<std::string>(second_me.address));
+            EXPECT_EQ(client.consumer_groups_count, second_me.consumer_groups_count);
+        }
+    }
+
+    EXPECT_TRUE(found_first);
+    EXPECT_TRUE(found_second);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsIsStableAcrossBackToBackCalls) {
+    RecordProperty("description", "Returns stable client lists across back-to-back get_clients calls.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    rust::Vec<iggy::ffi::ClientInfo> first_clients;
+    rust::Vec<iggy::ffi::ClientInfo> second_clients;
+    ASSERT_NO_THROW({
+        first_clients  = first_client->get_clients();
+        second_clients = first_client->get_clients();
+    });
+
+    ASSERT_EQ(second_clients.size(), first_clients.size());
+    for (const auto &first_entry : first_clients) {
+        bool found_match = false;
+        for (const auto &second_entry : second_clients) {
+            if (second_entry.client_id != first_entry.client_id) {
+                continue;
+            }
+
+            found_match = true;
+            EXPECT_EQ(second_entry.user_id, first_entry.user_id);
+            EXPECT_EQ(static_cast<std::string>(second_entry.address), static_cast<std::string>(first_entry.address));
+            EXPECT_EQ(static_cast<std::string>(second_entry.transport),
+                      static_cast<std::string>(first_entry.transport));
+            EXPECT_EQ(second_entry.consumer_groups_count, first_entry.consumer_groups_count);
+            break;
+        }
+        EXPECT_TRUE(found_match);
+    }
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsMatchesGetClientForReturnedIds) {
+    RecordProperty("description", "Returns list entries that agree with get_client for each returned client id.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    rust::Vec<iggy::ffi::ClientInfo> clients;
+    ASSERT_NO_THROW({ clients = first_client->get_clients(); });
+    ASSERT_GE(clients.size(), 2u);
+
+    for (const auto &client : clients) {
+        SCOPED_TRACE(client.client_id);
+        iggy::ffi::ClientInfoDetails details{};
+        ASSERT_NO_THROW({ details = first_client->get_client(client.client_id); });
+
+        EXPECT_EQ(details.client_id, client.client_id);
+        EXPECT_EQ(details.user_id, client.user_id);
+        EXPECT_EQ(static_cast<std::string>(details.address), static_cast<std::string>(client.address));
+        EXPECT_EQ(static_cast<std::string>(details.transport), static_cast<std::string>(client.transport));
+        EXPECT_EQ(details.consumer_groups_count, client.consumer_groups_count);
+    }
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsReflectsAdditionalSession) {
+    RecordProperty("description", "Reflects a newly added authenticated session in subsequent get_clients results.");
+    iggy::ffi::Client *first_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+
+    rust::Vec<iggy::ffi::ClientInfo> clients_before;
+    ASSERT_NO_THROW({ clients_before = first_client->get_clients(); });
+
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails second_me{};
+    rust::Vec<iggy::ffi::ClientInfo> clients_after;
+    ASSERT_NO_THROW({
+        second_me     = second_client->get_me();
+        clients_after = first_client->get_clients();
+    });
+
+    bool found_before = false;
+    for (const auto &client : clients_before) {
+        if (client.client_id == second_me.client_id) {
+            found_before = true;
+            break;
+        }
+    }
+    EXPECT_FALSE(found_before);
+
+    bool found_after = false;
+    for (const auto &client : clients_after) {
+        if (client.client_id != second_me.client_id) {
+            continue;
+        }
+
+        found_after = true;
+        EXPECT_EQ(client.user_id, second_me.user_id);
+        EXPECT_EQ(static_cast<std::string>(client.address), static_cast<std::string>(second_me.address));
+        EXPECT_EQ(static_cast<std::string>(client.transport), "TCP");
+        EXPECT_EQ(client.consumer_groups_count, second_me.consumer_groups_count);
+        break;
+    }
+    EXPECT_TRUE(found_after);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, PingSucceedsForNewConnection) {
+    RecordProperty("description", "Successfully pings the server from a fresh unauthenticated client session.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_NO_THROW(client->ping());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, HeartbeatIntervalReturnsDefaultValueForNewConnection) {
+    RecordProperty("description",
+                   "Returns the default heartbeat interval in microseconds for a fresh unauthenticated client.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    const auto heartbeat_interval = client->heartbeat_interval();
+    EXPECT_EQ(heartbeat_interval, 5'000'000u);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, HeartbeatIntervalReturnsConfiguredValueFromConnectionString) {
+    RecordProperty("description",
+                   "Returns the configured heartbeat interval in microseconds from the connection string.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection("iggy://iggy:iggy@127.0.0.1:8090?heartbeat_interval=10s"); });
+    ASSERT_NE(client, nullptr);
+
+    const auto heartbeat_interval = client->heartbeat_interval();
+    EXPECT_EQ(heartbeat_interval, 10'000'000u);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, SnapshotBeforeLoginThrows) {

Review Comment:
   all four snapshot tests (L763, L782, L797, L811) are negative paths. add a happy-path test with `"deflated"` + `["test"]` asserting the returned `Vec<u8>` is non-empty.



##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -149,3 +153,671 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add a test to create some streams, topics, partitions, and segments, send messages, and create
+// consumer groups and verify it.
+TEST(LowLevelE2E_Client, GetStatsReturnsServerStats) {
+    RecordProperty("description",
+                   "Returns empty resource counts first, then reflects aggregated streams, topics, partitions, "
+                   "consumer groups, and clients.");
+    const std::string first_stream_name                 = "cpp-get-stats-stream-1";
+    const std::string second_stream_name                = "cpp-get-stats-stream-2";
+    const std::string first_topic_name                  = "cpp-get-stats-topic-1";
+    const std::string second_topic_name                 = "cpp-get-stats-topic-2";
+    const std::string third_topic_name                  = "cpp-get-stats-topic-3";
+    const std::string first_group_name                  = "cpp-get-stats-group-1";
+    const std::string second_group_name                 = "cpp-get-stats-group-2";
+    const std::string third_group_name                  = "cpp-get-stats-group-3";
+    constexpr std::uint32_t additional_partitions_count = 2;
+    iggy::ffi::Client *client                           = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Client *second_client = nullptr;
+    iggy::ffi::Client *third_client  = nullptr;
+
+    iggy::ffi::Stats empty_stats{};
+    ASSERT_NO_THROW({
+        empty_stats = client->get_stats();
+        EXPECT_NE(empty_stats.process_id, 0u);
+        EXPECT_GT(empty_stats.threads_count, 0u);
+        EXPECT_GT(empty_stats.total_memory, 0u);
+        EXPECT_GE(empty_stats.available_memory, 0u);
+        EXPECT_GE(empty_stats.total_disk_space, empty_stats.free_disk_space);
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.hostname).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_name).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.kernel_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.iggy_server_version).empty());
+        EXPECT_TRUE(empty_stats.iggy_server_semver == std::numeric_limits<std::uint32_t>::max() ||
+                    empty_stats.iggy_server_semver > 0u);
+        EXPECT_GE(empty_stats.cache_metrics.size(), 0u);
+        EXPECT_EQ(empty_stats.streams_count, 0u);
+        EXPECT_EQ(empty_stats.topics_count, 0u);
+        EXPECT_EQ(empty_stats.partitions_count, 0u);
+        EXPECT_EQ(empty_stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(client->create_stream(first_stream_name));
+    ASSERT_NO_THROW(client->create_stream(second_stream_name));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), first_topic_name, 1, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), second_topic_name, 2, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(second_stream_name), third_topic_name, 3, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_partitions(make_string_identifier(first_stream_name),
+                                              make_string_identifier(first_topic_name), additional_partitions_count));
+    const auto first_group  = client->create_consumer_group(make_string_identifier(first_stream_name),
+                                                            make_string_identifier(first_topic_name), first_group_name);
+    const auto second_group = client->create_consumer_group(
+        make_string_identifier(first_stream_name), make_string_identifier(second_topic_name), second_group_name);
+    const auto third_group = client->create_consumer_group(make_string_identifier(second_stream_name),
+                                                           make_string_identifier(third_topic_name), third_group_name);
+
+    ASSERT_NO_THROW({ second_client = login_to_server(); });
+    ASSERT_NE(second_client, nullptr);
+    ASSERT_NO_THROW({ third_client = login_to_server(); });
+    ASSERT_NE(third_client, nullptr);
+
+    const auto first_stream_details           = client->get_stream(make_string_identifier(first_stream_name));
+    const auto second_stream_details          = client->get_stream(make_string_identifier(second_stream_name));
+    const std::uint32_t expected_topics_count = first_stream_details.topics_count + second_stream_details.topics_count;
+    std::uint32_t first_topic_partitions      = 0;
+    std::uint32_t second_topic_partitions     = 0;
+    std::uint32_t third_topic_partitions      = 0;
+    for (const auto &topic : first_stream_details.topics) {
+        if (topic.name == first_topic_name) {
+            first_topic_partitions = topic.partitions_count;
+        }
+        if (topic.name == second_topic_name) {
+            second_topic_partitions = topic.partitions_count;
+        }
+    }
+    for (const auto &topic : second_stream_details.topics) {
+        if (topic.name == third_topic_name) {
+            third_topic_partitions = topic.partitions_count;
+        }
+    }
+    const std::uint32_t expected_partitions_count =
+        first_topic_partitions + second_topic_partitions + third_topic_partitions;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 2u);
+        EXPECT_EQ(stats.topics_count, expected_topics_count);
+        EXPECT_EQ(stats.partitions_count, expected_partitions_count);
+        EXPECT_EQ(stats.segments_count, expected_partitions_count);
+        EXPECT_EQ(stats.consumer_groups_count, 3u);
+        EXPECT_EQ(stats.clients_count, empty_stats.clients_count + 2u);
+        EXPECT_EQ(first_group.partitions_count, first_topic_partitions);
+        EXPECT_EQ(second_group.partitions_count, second_topic_partitions);
+        EXPECT_EQ(third_group.partitions_count, third_topic_partitions);
+    });
+
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(second_stream_name)));
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(first_stream_name)));
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(third_client));
+    third_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 0u);
+        EXPECT_EQ(stats.topics_count, 0u);
+        EXPECT_EQ(stats.partitions_count, 0u);
+        EXPECT_EQ(stats.segments_count, 0u);
+        EXPECT_EQ(stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetStatsIsStableAcrossBackToBackCalls) {
+    RecordProperty(
+        "description",
+        "Returns sane invariant fields across back-to-back get_stats calls on an idle authenticated client.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Stats first_stats{};
+    iggy::ffi::Stats second_stats{};
+    ASSERT_NO_THROW({
+        first_stats  = client->get_stats();
+        second_stats = client->get_stats();
+    });
+
+    EXPECT_NE(first_stats.process_id, 0u);
+    EXPECT_NE(second_stats.process_id, 0u);
+    EXPECT_EQ(second_stats.process_id, first_stats.process_id);
+    EXPECT_GT(first_stats.threads_count, 0u);
+    EXPECT_GT(second_stats.threads_count, 0u);
+    EXPECT_GT(first_stats.total_memory, 0u);
+    EXPECT_GT(second_stats.total_memory, 0u);
+    EXPECT_FALSE(static_cast<std::string>(first_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.iggy_server_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.iggy_server_version).empty());
+    EXPECT_EQ(static_cast<std::string>(second_stats.hostname), static_cast<std::string>(first_stats.hostname));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_name), static_cast<std::string>(first_stats.os_name));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_version), static_cast<std::string>(first_stats.os_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.kernel_version),
+              static_cast<std::string>(first_stats.kernel_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.iggy_server_version),
+              static_cast<std::string>(first_stats.iggy_server_version));
+    EXPECT_EQ(second_stats.iggy_server_semver, first_stats.iggy_server_semver);
+    EXPECT_EQ(second_stats.clients_count, first_stats.clients_count);
+    EXPECT_EQ(second_stats.streams_count, first_stats.streams_count);
+    EXPECT_EQ(second_stats.topics_count, first_stats.topics_count);
+    EXPECT_EQ(second_stats.partitions_count, first_stats.partitions_count);
+    EXPECT_EQ(second_stats.consumer_groups_count, first_stats.consumer_groups_count);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_me before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_me(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_me(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add additional validation for get_me after merging join_consumer_group PR.
+TEST(LowLevelE2E_Client, GetMeReturnsCurrentClientDetails) {
+    RecordProperty("description", "Returns the current authenticated client details.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_NO_THROW({
+        const auto me = client->get_me();
+        EXPECT_NE(me.client_id, 0u);
+        EXPECT_NE(me.user_id, std::numeric_limits<std::uint32_t>::max());
+        EXPECT_FALSE(static_cast<std::string>(me.address).empty());
+        EXPECT_EQ(static_cast<std::string>(me.transport), "TCP");
+        EXPECT_EQ(me.consumer_groups_count, 0u);
+        EXPECT_TRUE(me.consumer_groups.empty());
+    });
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeIsStableAcrossBackToBackCalls) {
+    RecordProperty("description", "Returns stable current-client details across back-to-back get_me calls.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({
+        first_me  = client->get_me();
+        second_me = client->get_me();
+    });
+
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_EQ(second_me.client_id, first_me.client_id);
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(second_me.address), static_cast<std::string>(first_me.address));
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), static_cast<std::string>(first_me.transport));
+    EXPECT_EQ(second_me.consumer_groups_count, first_me.consumer_groups_count);
+    EXPECT_EQ(second_me.consumer_groups.size(), first_me.consumer_groups.size());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeReturnsDistinctClientIdsForDifferentSessions) {
+    RecordProperty(
+        "description",
+        "Returns different client ids for separate authenticated sessions while keeping the same user identity.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({
+        first_me  = first_client->get_me();
+        second_me = second_client->get_me();
+    });
+
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_NE(second_me.client_id, 0u);
+    EXPECT_NE(second_me.client_id, first_me.client_id);
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeReturnsValidDetailsAfterReconnect) {
+    RecordProperty("description",
+                   "Returns valid current-client details after reconnecting with a fresh authenticated session.");
+    iggy::ffi::Client *first_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    ASSERT_NO_THROW({ first_me = first_client->get_me(); });
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_NE(first_me.user_id, std::numeric_limits<std::uint32_t>::max());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({ second_me = second_client->get_me(); });
+    EXPECT_NE(second_me.client_id, 0u);
+    EXPECT_NE(second_me.user_id, std::numeric_limits<std::uint32_t>::max());
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), static_cast<std::string>(first_me.transport));
+    EXPECT_FALSE(static_cast<std::string>(second_me.address).empty());
+    EXPECT_EQ(second_me.consumer_groups_count, 0u);
+    EXPECT_TRUE(second_me.consumer_groups.empty());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_client before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_client(1), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_client(1), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientWithWrongClientIdThrows) {
+    RecordProperty("description", "Rejects querying invalid or non-existent client ids.");
+    const std::uint32_t wrong_client_ids[] = {0u, std::numeric_limits<std::uint32_t>::max()};

Review Comment:
   `wrong_client_ids = {0u, max()}` - `max()` IS the sentinel returned when user_id is missing. test conflates `invalid id` with `missing user`. use a different unreachable id (e.g. `last_id_from_get_clients() + 1`).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3161474311


##########
foreign/cpp/src/lib.rs:
##########
@@ -75,6 +75,69 @@ mod ffi {
         members: Vec<ConsumerGroupMember>,
     }
 
+    struct ConsumerGroupInfo {
+        stream_id: u32,
+        topic_id: u32,
+        group_id: u32,
+    }
+
+    struct ClientInfo {
+        client_id: u32,
+        user_id: u32,

Review Comment:
   fixed



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3161579355


##########
foreign/cpp/src/client.rs:
##########
@@ -378,6 +479,98 @@ impl Client {
             Ok(())
         })
     }
+
+    pub fn get_stats(&self) -> Result<ffi::Stats, String> {
+        RUNTIME.block_on(async {
+            let stats = self
+                .inner
+                .get_stats()
+                .await
+                .map_err(|error| format!("Could not get stats: {error}"))?;
+            Ok(ffi::Stats::from(stats))
+        })
+    }
+
+    pub fn get_me(&self) -> Result<ffi::ClientInfoDetails, String> {
+        RUNTIME.block_on(async {
+            let client = self
+                .inner
+                .get_me()
+                .await
+                .map_err(|error| format!("Could not get current client info: {error}"))?;
+            Ok(ffi::ClientInfoDetails::from(client))
+        })
+    }
+
+    pub fn get_client(&self, client_id: u32) -> Result<ffi::ClientInfoDetails, String> {
+        RUNTIME.block_on(async {
+            let client = self
+                .inner
+                .get_client(client_id)
+                .await
+                .map_err(|error| format!("Could not get client '{client_id}': {error}"))?;
+            ffi::ClientInfoDetails::try_from(client)
+                .map_err(|error| format!("Could not get client '{client_id}': {error}"))
+        })
+    }
+
+    pub fn get_clients(&self) -> Result<Vec<ffi::ClientInfo>, String> {
+        RUNTIME.block_on(async {
+            let clients = self
+                .inner
+                .get_clients()
+                .await
+                .map_err(|error| format!("Could not get clients: {error}"))?;
+            Ok(clients.into_iter().map(ffi::ClientInfo::from).collect())
+        })
+    }
+
+    pub fn ping(&self) -> Result<(), String> {
+        RUNTIME.block_on(async {
+            self.inner
+                .ping()
+                .await
+                .map_err(|error| format!("Could not ping server: {error}"))?;
+            Ok(())
+        })
+    }
+
+    pub fn heartbeat_interval(&self) -> u64 {
+        RUNTIME.block_on(async { self.inner.heartbeat_interval().await.as_micros() })
+    }
+
+    pub fn snapshot(
+        &self,
+        snapshot_compression: String,
+        snapshot_types: Vec<String>,
+    ) -> Result<Vec<u8>, String> {
+        let rust_compression = match snapshot_compression.trim() {
+            "" => RustSnapshotCompression::default(),

Review Comment:
   client now rejects empty value for snapshot compression



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3161545609


##########
foreign/cpp/src/client.rs:
##########
@@ -378,6 +479,98 @@ impl Client {
             Ok(())
         })
     }
+
+    pub fn get_stats(&self) -> Result<ffi::Stats, String> {
+        RUNTIME.block_on(async {
+            let stats = self
+                .inner
+                .get_stats()
+                .await
+                .map_err(|error| format!("Could not get stats: {error}"))?;
+            Ok(ffi::Stats::from(stats))
+        })
+    }
+
+    pub fn get_me(&self) -> Result<ffi::ClientInfoDetails, String> {
+        RUNTIME.block_on(async {
+            let client = self
+                .inner
+                .get_me()
+                .await
+                .map_err(|error| format!("Could not get current client info: {error}"))?;
+            Ok(ffi::ClientInfoDetails::from(client))
+        })
+    }
+
+    pub fn get_client(&self, client_id: u32) -> Result<ffi::ClientInfoDetails, String> {
+        RUNTIME.block_on(async {
+            let client = self
+                .inner
+                .get_client(client_id)
+                .await
+                .map_err(|error| format!("Could not get client '{client_id}': {error}"))?;
+            ffi::ClientInfoDetails::try_from(client)
+                .map_err(|error| format!("Could not get client '{client_id}': {error}"))
+        })
+    }
+
+    pub fn get_clients(&self) -> Result<Vec<ffi::ClientInfo>, String> {
+        RUNTIME.block_on(async {
+            let clients = self
+                .inner
+                .get_clients()
+                .await
+                .map_err(|error| format!("Could not get clients: {error}"))?;
+            Ok(clients.into_iter().map(ffi::ClientInfo::from).collect())
+        })
+    }
+
+    pub fn ping(&self) -> Result<(), String> {
+        RUNTIME.block_on(async {
+            self.inner
+                .ping()
+                .await
+                .map_err(|error| format!("Could not ping server: {error}"))?;
+            Ok(())
+        })
+    }
+
+    pub fn heartbeat_interval(&self) -> u64 {
+        RUNTIME.block_on(async { self.inner.heartbeat_interval().await.as_micros() })

Review Comment:
   done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3161534407


##########
foreign/cpp/src/lib.rs:
##########
@@ -75,6 +75,69 @@ mod ffi {
         members: Vec<ConsumerGroupMember>,
     }
 
+    struct ConsumerGroupInfo {
+        stream_id: u32,
+        topic_id: u32,
+        group_id: u32,
+    }
+
+    struct ClientInfo {
+        client_id: u32,
+        user_id: u32,
+        address: String,
+        transport: String,
+        consumer_groups_count: u32,
+    }
+
+    struct ClientInfoDetails {
+        client_id: u32,
+        user_id: u32,
+        address: String,
+        transport: String,
+        consumer_groups_count: u32,
+        consumer_groups: Vec<ConsumerGroupInfo>,
+    }
+
+    struct CacheMetricEntry {
+        stream_id: u32,
+        topic_id: u32,
+        partition_id: u32,
+        hits: u64,
+        misses: u64,
+        hit_ratio: f32,
+    }
+
+    struct Stats {
+        process_id: u32,
+        cpu_usage: f32,
+        total_cpu_usage: f32,
+        memory_usage: u64,
+        total_memory: u64,
+        available_memory: u64,
+        run_time: u64,
+        start_time: u64,
+        read_bytes: u64,
+        written_bytes: u64,
+        messages_size_bytes: u64,
+        streams_count: u32,
+        topics_count: u32,
+        partitions_count: u32,
+        segments_count: u32,
+        messages_count: u64,
+        clients_count: u32,
+        consumer_groups_count: u32,
+        hostname: String,
+        os_name: String,
+        os_version: String,
+        kernel_version: String,
+        iggy_server_version: String,
+        iggy_server_semver: u32,

Review Comment:
   done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3161720360


##########
foreign/cpp/tests/client/low_level_e2e.cpp:
##########
@@ -149,3 +153,671 @@ TEST(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     iggy::ffi::Client *client = nullptr;
     ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
 }
+
+TEST(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_stats before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add a test to create some streams, topics, partitions, and segments, send messages, and create
+// consumer groups and verify it.
+TEST(LowLevelE2E_Client, GetStatsReturnsServerStats) {
+    RecordProperty("description",
+                   "Returns empty resource counts first, then reflects aggregated streams, topics, partitions, "
+                   "consumer groups, and clients.");
+    const std::string first_stream_name                 = "cpp-get-stats-stream-1";
+    const std::string second_stream_name                = "cpp-get-stats-stream-2";
+    const std::string first_topic_name                  = "cpp-get-stats-topic-1";
+    const std::string second_topic_name                 = "cpp-get-stats-topic-2";
+    const std::string third_topic_name                  = "cpp-get-stats-topic-3";
+    const std::string first_group_name                  = "cpp-get-stats-group-1";
+    const std::string second_group_name                 = "cpp-get-stats-group-2";
+    const std::string third_group_name                  = "cpp-get-stats-group-3";
+    constexpr std::uint32_t additional_partitions_count = 2;
+    iggy::ffi::Client *client                           = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Client *second_client = nullptr;
+    iggy::ffi::Client *third_client  = nullptr;
+
+    iggy::ffi::Stats empty_stats{};
+    ASSERT_NO_THROW({
+        empty_stats = client->get_stats();
+        EXPECT_NE(empty_stats.process_id, 0u);
+        EXPECT_GT(empty_stats.threads_count, 0u);
+        EXPECT_GT(empty_stats.total_memory, 0u);
+        EXPECT_GE(empty_stats.available_memory, 0u);
+        EXPECT_GE(empty_stats.total_disk_space, empty_stats.free_disk_space);
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.hostname).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_name).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.os_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.kernel_version).empty());
+        EXPECT_FALSE(static_cast<std::string>(empty_stats.iggy_server_version).empty());
+        EXPECT_TRUE(empty_stats.iggy_server_semver == std::numeric_limits<std::uint32_t>::max() ||
+                    empty_stats.iggy_server_semver > 0u);
+        EXPECT_GE(empty_stats.cache_metrics.size(), 0u);
+        EXPECT_EQ(empty_stats.streams_count, 0u);
+        EXPECT_EQ(empty_stats.topics_count, 0u);
+        EXPECT_EQ(empty_stats.partitions_count, 0u);
+        EXPECT_EQ(empty_stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(client->create_stream(first_stream_name));
+    ASSERT_NO_THROW(client->create_stream(second_stream_name));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), first_topic_name, 1, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), second_topic_name, 2, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(second_stream_name), third_topic_name, 3, "none", 0,
+                                         "server_default", 0, "server_default"));
+    ASSERT_NO_THROW(client->create_partitions(make_string_identifier(first_stream_name),
+                                              make_string_identifier(first_topic_name), additional_partitions_count));
+    const auto first_group  = client->create_consumer_group(make_string_identifier(first_stream_name),
+                                                            make_string_identifier(first_topic_name), first_group_name);
+    const auto second_group = client->create_consumer_group(
+        make_string_identifier(first_stream_name), make_string_identifier(second_topic_name), second_group_name);
+    const auto third_group = client->create_consumer_group(make_string_identifier(second_stream_name),
+                                                           make_string_identifier(third_topic_name), third_group_name);
+
+    ASSERT_NO_THROW({ second_client = login_to_server(); });
+    ASSERT_NE(second_client, nullptr);
+    ASSERT_NO_THROW({ third_client = login_to_server(); });
+    ASSERT_NE(third_client, nullptr);
+
+    const auto first_stream_details           = client->get_stream(make_string_identifier(first_stream_name));
+    const auto second_stream_details          = client->get_stream(make_string_identifier(second_stream_name));
+    const std::uint32_t expected_topics_count = first_stream_details.topics_count + second_stream_details.topics_count;
+    std::uint32_t first_topic_partitions      = 0;
+    std::uint32_t second_topic_partitions     = 0;
+    std::uint32_t third_topic_partitions      = 0;
+    for (const auto &topic : first_stream_details.topics) {
+        if (topic.name == first_topic_name) {
+            first_topic_partitions = topic.partitions_count;
+        }
+        if (topic.name == second_topic_name) {
+            second_topic_partitions = topic.partitions_count;
+        }
+    }
+    for (const auto &topic : second_stream_details.topics) {
+        if (topic.name == third_topic_name) {
+            third_topic_partitions = topic.partitions_count;
+        }
+    }
+    const std::uint32_t expected_partitions_count =
+        first_topic_partitions + second_topic_partitions + third_topic_partitions;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 2u);
+        EXPECT_EQ(stats.topics_count, expected_topics_count);
+        EXPECT_EQ(stats.partitions_count, expected_partitions_count);
+        EXPECT_EQ(stats.segments_count, expected_partitions_count);
+        EXPECT_EQ(stats.consumer_groups_count, 3u);
+        EXPECT_EQ(stats.clients_count, empty_stats.clients_count + 2u);
+        EXPECT_EQ(first_group.partitions_count, first_topic_partitions);
+        EXPECT_EQ(second_group.partitions_count, second_topic_partitions);
+        EXPECT_EQ(third_group.partitions_count, third_topic_partitions);
+    });
+
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(second_stream_name)));
+    ASSERT_NO_THROW(client->delete_stream(make_string_identifier(first_stream_name)));
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(third_client));
+    third_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+
+    ASSERT_NO_THROW({
+        const auto stats = client->get_stats();
+        EXPECT_EQ(stats.streams_count, 0u);
+        EXPECT_EQ(stats.topics_count, 0u);
+        EXPECT_EQ(stats.partitions_count, 0u);
+        EXPECT_EQ(stats.segments_count, 0u);
+        EXPECT_EQ(stats.consumer_groups_count, 0u);
+    });
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetStatsIsStableAcrossBackToBackCalls) {
+    RecordProperty(
+        "description",
+        "Returns sane invariant fields across back-to-back get_stats calls on an idle authenticated client.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::Stats first_stats{};
+    iggy::ffi::Stats second_stats{};
+    ASSERT_NO_THROW({
+        first_stats  = client->get_stats();
+        second_stats = client->get_stats();
+    });
+
+    EXPECT_NE(first_stats.process_id, 0u);
+    EXPECT_NE(second_stats.process_id, 0u);
+    EXPECT_EQ(second_stats.process_id, first_stats.process_id);
+    EXPECT_GT(first_stats.threads_count, 0u);
+    EXPECT_GT(second_stats.threads_count, 0u);
+    EXPECT_GT(first_stats.total_memory, 0u);
+    EXPECT_GT(second_stats.total_memory, 0u);
+    EXPECT_FALSE(static_cast<std::string>(first_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.hostname).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_name).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.os_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.kernel_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(first_stats.iggy_server_version).empty());
+    EXPECT_FALSE(static_cast<std::string>(second_stats.iggy_server_version).empty());
+    EXPECT_EQ(static_cast<std::string>(second_stats.hostname), static_cast<std::string>(first_stats.hostname));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_name), static_cast<std::string>(first_stats.os_name));
+    EXPECT_EQ(static_cast<std::string>(second_stats.os_version), static_cast<std::string>(first_stats.os_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.kernel_version),
+              static_cast<std::string>(first_stats.kernel_version));
+    EXPECT_EQ(static_cast<std::string>(second_stats.iggy_server_version),
+              static_cast<std::string>(first_stats.iggy_server_version));
+    EXPECT_EQ(second_stats.iggy_server_semver, first_stats.iggy_server_semver);
+    EXPECT_EQ(second_stats.clients_count, first_stats.clients_count);
+    EXPECT_EQ(second_stats.streams_count, first_stats.streams_count);
+    EXPECT_EQ(second_stats.topics_count, first_stats.topics_count);
+    EXPECT_EQ(second_stats.partitions_count, first_stats.partitions_count);
+    EXPECT_EQ(second_stats.consumer_groups_count, first_stats.consumer_groups_count);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_me before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_me(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_me(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+// TODO(slbotbm-this PR): add additional validation for get_me after merging join_consumer_group PR.
+TEST(LowLevelE2E_Client, GetMeReturnsCurrentClientDetails) {
+    RecordProperty("description", "Returns the current authenticated client details.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_NO_THROW({
+        const auto me = client->get_me();
+        EXPECT_NE(me.client_id, 0u);
+        EXPECT_NE(me.user_id, std::numeric_limits<std::uint32_t>::max());
+        EXPECT_FALSE(static_cast<std::string>(me.address).empty());
+        EXPECT_EQ(static_cast<std::string>(me.transport), "TCP");
+        EXPECT_EQ(me.consumer_groups_count, 0u);
+        EXPECT_TRUE(me.consumer_groups.empty());
+    });
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeIsStableAcrossBackToBackCalls) {
+    RecordProperty("description", "Returns stable current-client details across back-to-back get_me calls.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({
+        first_me  = client->get_me();
+        second_me = client->get_me();
+    });
+
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_EQ(second_me.client_id, first_me.client_id);
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(second_me.address), static_cast<std::string>(first_me.address));
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), static_cast<std::string>(first_me.transport));
+    EXPECT_EQ(second_me.consumer_groups_count, first_me.consumer_groups_count);
+    EXPECT_EQ(second_me.consumer_groups.size(), first_me.consumer_groups.size());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeReturnsDistinctClientIdsForDifferentSessions) {
+    RecordProperty(
+        "description",
+        "Returns different client ids for separate authenticated sessions while keeping the same user identity.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({
+        first_me  = first_client->get_me();
+        second_me = second_client->get_me();
+    });
+
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_NE(second_me.client_id, 0u);
+    EXPECT_NE(second_me.client_id, first_me.client_id);
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetMeReturnsValidDetailsAfterReconnect) {
+    RecordProperty("description",
+                   "Returns valid current-client details after reconnecting with a fresh authenticated session.");
+    iggy::ffi::Client *first_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    ASSERT_NO_THROW({ first_me = first_client->get_me(); });
+    EXPECT_NE(first_me.client_id, 0u);
+    EXPECT_NE(first_me.user_id, std::numeric_limits<std::uint32_t>::max());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails second_me{};
+    ASSERT_NO_THROW({ second_me = second_client->get_me(); });
+    EXPECT_NE(second_me.client_id, 0u);
+    EXPECT_NE(second_me.user_id, std::numeric_limits<std::uint32_t>::max());
+    EXPECT_EQ(second_me.user_id, first_me.user_id);
+    EXPECT_EQ(static_cast<std::string>(first_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_me.transport), static_cast<std::string>(first_me.transport));
+    EXPECT_FALSE(static_cast<std::string>(second_me.address).empty());
+    EXPECT_EQ(second_me.consumer_groups_count, 0u);
+    EXPECT_TRUE(second_me.consumer_groups.empty());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_client before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_client(1), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_client(1), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientWithWrongClientIdThrows) {
+    RecordProperty("description", "Rejects querying invalid or non-existent client ids.");
+    const std::uint32_t wrong_client_ids[] = {0u, std::numeric_limits<std::uint32_t>::max()};
+    iggy::ffi::Client *client              = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    for (const std::uint32_t wrong_client_id : wrong_client_ids) {
+        SCOPED_TRACE(wrong_client_id);
+        ASSERT_THROW(client->get_client(wrong_client_id), std::exception);
+    }
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientReturnsDetailsForMatchingClientId) {
+    RecordProperty("description", "Returns current client details when querying with the authenticated client id.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::ClientInfoDetails current_client{};
+    iggy::ffi::ClientInfoDetails looked_up_client{};
+    ASSERT_NO_THROW({
+        current_client   = client->get_me();
+        looked_up_client = client->get_client(current_client.client_id);
+    });
+
+    EXPECT_NE(current_client.client_id, 0u);
+    EXPECT_EQ(looked_up_client.client_id, current_client.client_id);
+    EXPECT_EQ(looked_up_client.user_id, current_client.user_id);
+    EXPECT_EQ(static_cast<std::string>(looked_up_client.address), static_cast<std::string>(current_client.address));
+    EXPECT_EQ(static_cast<std::string>(looked_up_client.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(looked_up_client.transport), static_cast<std::string>(current_client.transport));
+    EXPECT_EQ(looked_up_client.consumer_groups_count, current_client.consumer_groups_count);
+    EXPECT_EQ(looked_up_client.consumer_groups.size(), current_client.consumer_groups.size());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientIsStableAcrossBackToBackCalls) {
+    RecordProperty("description", "Returns stable client details across back-to-back get_client calls.");
+    iggy::ffi::Client *client = login_to_server();
+    ASSERT_NE(client, nullptr);
+
+    iggy::ffi::ClientInfoDetails current_client{};
+    iggy::ffi::ClientInfoDetails first_lookup{};
+    iggy::ffi::ClientInfoDetails second_lookup{};
+    ASSERT_NO_THROW({
+        current_client = client->get_me();
+        first_lookup   = client->get_client(current_client.client_id);
+        second_lookup  = client->get_client(current_client.client_id);
+    });
+
+    EXPECT_NE(current_client.client_id, 0u);
+    EXPECT_EQ(first_lookup.client_id, current_client.client_id);
+    EXPECT_EQ(second_lookup.client_id, first_lookup.client_id);
+    EXPECT_EQ(second_lookup.user_id, first_lookup.user_id);
+    EXPECT_EQ(static_cast<std::string>(second_lookup.address), static_cast<std::string>(first_lookup.address));
+    EXPECT_EQ(static_cast<std::string>(first_lookup.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_lookup.transport), "TCP");
+    EXPECT_EQ(static_cast<std::string>(second_lookup.transport), static_cast<std::string>(first_lookup.transport));
+    EXPECT_EQ(second_lookup.consumer_groups_count, first_lookup.consumer_groups_count);
+    EXPECT_EQ(second_lookup.consumer_groups.size(), first_lookup.consumer_groups.size());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsBeforeLoginThrows) {
+    RecordProperty("description", "Rejects get_clients before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_THROW(client->get_clients(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_clients(), std::exception);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsReturnsActiveClientSessions) {
+    RecordProperty("description", "Returns the currently active authenticated client sessions.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails second_me{};
+    rust::Vec<iggy::ffi::ClientInfo> clients;
+    ASSERT_NO_THROW({
+        first_me  = first_client->get_me();
+        second_me = second_client->get_me();
+        clients   = first_client->get_clients();
+    });
+
+    ASSERT_GE(clients.size(), 2u);
+
+    bool found_first  = false;
+    bool found_second = false;
+    for (const auto &client : clients) {
+        EXPECT_NE(client.client_id, 0u);
+        EXPECT_EQ(static_cast<std::string>(client.transport), "TCP");
+
+        if (client.client_id == first_me.client_id) {
+            found_first = true;
+            EXPECT_EQ(client.user_id, first_me.user_id);
+            EXPECT_EQ(static_cast<std::string>(client.address), static_cast<std::string>(first_me.address));
+            EXPECT_EQ(client.consumer_groups_count, first_me.consumer_groups_count);
+        }
+
+        if (client.client_id == second_me.client_id) {
+            found_second = true;
+            EXPECT_EQ(client.user_id, second_me.user_id);
+            EXPECT_EQ(static_cast<std::string>(client.address), static_cast<std::string>(second_me.address));
+            EXPECT_EQ(client.consumer_groups_count, second_me.consumer_groups_count);
+        }
+    }
+
+    EXPECT_TRUE(found_first);
+    EXPECT_TRUE(found_second);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsIsStableAcrossBackToBackCalls) {
+    RecordProperty("description", "Returns stable client lists across back-to-back get_clients calls.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    rust::Vec<iggy::ffi::ClientInfo> first_clients;
+    rust::Vec<iggy::ffi::ClientInfo> second_clients;
+    ASSERT_NO_THROW({
+        first_clients  = first_client->get_clients();
+        second_clients = first_client->get_clients();
+    });
+
+    ASSERT_EQ(second_clients.size(), first_clients.size());
+    for (const auto &first_entry : first_clients) {
+        bool found_match = false;
+        for (const auto &second_entry : second_clients) {
+            if (second_entry.client_id != first_entry.client_id) {
+                continue;
+            }
+
+            found_match = true;
+            EXPECT_EQ(second_entry.user_id, first_entry.user_id);
+            EXPECT_EQ(static_cast<std::string>(second_entry.address), static_cast<std::string>(first_entry.address));
+            EXPECT_EQ(static_cast<std::string>(second_entry.transport),
+                      static_cast<std::string>(first_entry.transport));
+            EXPECT_EQ(second_entry.consumer_groups_count, first_entry.consumer_groups_count);
+            break;
+        }
+        EXPECT_TRUE(found_match);
+    }
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsMatchesGetClientForReturnedIds) {
+    RecordProperty("description", "Returns list entries that agree with get_client for each returned client id.");
+    iggy::ffi::Client *first_client  = login_to_server();
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+    ASSERT_NE(second_client, nullptr);
+
+    rust::Vec<iggy::ffi::ClientInfo> clients;
+    ASSERT_NO_THROW({ clients = first_client->get_clients(); });
+    ASSERT_GE(clients.size(), 2u);
+
+    for (const auto &client : clients) {
+        SCOPED_TRACE(client.client_id);
+        iggy::ffi::ClientInfoDetails details{};
+        ASSERT_NO_THROW({ details = first_client->get_client(client.client_id); });
+
+        EXPECT_EQ(details.client_id, client.client_id);
+        EXPECT_EQ(details.user_id, client.user_id);
+        EXPECT_EQ(static_cast<std::string>(details.address), static_cast<std::string>(client.address));
+        EXPECT_EQ(static_cast<std::string>(details.transport), static_cast<std::string>(client.transport));
+        EXPECT_EQ(details.consumer_groups_count, client.consumer_groups_count);
+    }
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, GetClientsReflectsAdditionalSession) {
+    RecordProperty("description", "Reflects a newly added authenticated session in subsequent get_clients results.");
+    iggy::ffi::Client *first_client = login_to_server();
+    ASSERT_NE(first_client, nullptr);
+
+    rust::Vec<iggy::ffi::ClientInfo> clients_before;
+    ASSERT_NO_THROW({ clients_before = first_client->get_clients(); });
+
+    iggy::ffi::Client *second_client = login_to_server();
+    ASSERT_NE(second_client, nullptr);
+
+    iggy::ffi::ClientInfoDetails second_me{};
+    rust::Vec<iggy::ffi::ClientInfo> clients_after;
+    ASSERT_NO_THROW({
+        second_me     = second_client->get_me();
+        clients_after = first_client->get_clients();
+    });
+
+    bool found_before = false;
+    for (const auto &client : clients_before) {
+        if (client.client_id == second_me.client_id) {
+            found_before = true;
+            break;
+        }
+    }
+    EXPECT_FALSE(found_before);
+
+    bool found_after = false;
+    for (const auto &client : clients_after) {
+        if (client.client_id != second_me.client_id) {
+            continue;
+        }
+
+        found_after = true;
+        EXPECT_EQ(client.user_id, second_me.user_id);
+        EXPECT_EQ(static_cast<std::string>(client.address), static_cast<std::string>(second_me.address));
+        EXPECT_EQ(static_cast<std::string>(client.transport), "TCP");
+        EXPECT_EQ(client.consumer_groups_count, second_me.consumer_groups_count);
+        break;
+    }
+    EXPECT_TRUE(found_after);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(second_client));
+    second_client = nullptr;
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(first_client));
+    first_client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, PingSucceedsForNewConnection) {
+    RecordProperty("description", "Successfully pings the server from a fresh unauthenticated client session.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_NO_THROW(client->ping());
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, HeartbeatIntervalReturnsDefaultValueForNewConnection) {
+    RecordProperty("description",
+                   "Returns the default heartbeat interval in microseconds for a fresh unauthenticated client.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    const auto heartbeat_interval = client->heartbeat_interval();
+    EXPECT_EQ(heartbeat_interval, 5'000'000u);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, HeartbeatIntervalReturnsConfiguredValueFromConnectionString) {
+    RecordProperty("description",
+                   "Returns the configured heartbeat interval in microseconds from the connection string.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection("iggy://iggy:iggy@127.0.0.1:8090?heartbeat_interval=10s"); });
+    ASSERT_NE(client, nullptr);
+
+    const auto heartbeat_interval = client->heartbeat_interval();
+    EXPECT_EQ(heartbeat_interval, 10'000'000u);
+
+    ASSERT_NO_THROW(iggy::ffi::delete_connection(client));
+    client = nullptr;
+}
+
+TEST(LowLevelE2E_Client, SnapshotBeforeLoginThrows) {
+    RecordProperty("description", "Rejects snapshot before connect, and after connect but before login.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    rust::Vec<rust::String> snapshot_types_before_connect;

Review Comment:
   done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3161687187


##########
foreign/cpp/src/identifier.rs:
##########
@@ -62,6 +62,7 @@ impl TryFrom<ffi::Identifier> for RustIdentifier {
     }
 }
 
+#[allow(clippy::wrong_self_convention)]

Review Comment:
   done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on PR #3102:
URL: https://github.com/apache/iggy/pull/3102#issuecomment-4466253010

   /ready


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on code in PR #3102:
URL: https://github.com/apache/iggy/pull/3102#discussion_r3293180748


##########
foreign/cpp/src/client.rs:
##########
@@ -21,15 +21,119 @@ use iggy::prelude::{
     ConsumerGroupClient, Identifier as RustIdentifier, IggyClient as RustIggyClient,
     IggyClientBuilder as RustIggyClientBuilder, IggyError, IggyExpiry as RustIggyExpiry,
     IggyMessage, IggyTimestamp, MaxTopicSize as RustMaxTopicSize, MessageClient, PartitionClient,
-    Partitioning, PollingStrategy, StreamClient, TopicClient, UserClient,
+    Partitioning, PollingStrategy, SnapshotCompression as RustSnapshotCompression, StreamClient,
+    SystemClient as RustSystemClient, SystemSnapshotType as RustSystemSnapshotType, TopicClient,
+    UserClient,
 };
+use iggy_common::{
+    CacheMetrics as RustCacheMetrics, CacheMetricsKey as RustCacheMetricsKey,
+    ClientInfo as RustClientInfo, ClientInfoDetails as RustClientInfoDetails, Stats as RustStats,
+};
+use std::convert::TryFrom;
 use std::str::FromStr;
 use std::sync::Arc;
 
 /// Sentinel value passed from C++ to mean "no partition specified" — the server picks the
 /// partition based on the consumer/strategy. Cxx FFI does not support `Option<u32>`, so we
 /// reserve `u32::MAX` as the sentinel for `partition_id`.
 const ANY_PARTITION_ID: u32 = u32::MAX;
+impl From<RustClientInfo> for ffi::ClientInfo {
+    fn from(client: RustClientInfo) -> Self {
+        let has_user_id = client.user_id.is_some();
+        ffi::ClientInfo {
+            client_id: client.client_id,
+            has_user_id,
+            user_id: client.user_id.unwrap_or(0),

Review Comment:
   Using u32::MAX



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(cpp): add functions related to system [iggy]

Posted by "slbotbm (via GitHub)" <gi...@apache.org>.
slbotbm commented on PR #3102:
URL: https://github.com/apache/iggy/pull/3102#issuecomment-4526074468

   /ready


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@iggy.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org