Bug fixes: Adds cache so reports aren't reported multiple times. Finally fixed #23#27
Conversation
There was a problem hiding this comment.
Pull request overview
This pull request fixes issue #23 where the endlessh-report tool incorrectly calculated connection counts and reported entries multiple times. The fix introduces a caching mechanism to track previously reported IPs and ensure they are only reported when their statistics change.
Changes:
- Fixed the critical bug where CLOSE connections were always counted as 0 (line 378 in
src/main.cpp) - Added a new
ReportCacheclass to persistently track IP statistics and prevent duplicate reports - Introduced new command-line options:
--strict,--cache-file,--sort, and--dns-lookup - Refactored IP normalization and DNS lookup functionality into a separate
endlessh_corelibrary - Added comprehensive unit and end-to-end tests with coverage reporting infrastructure
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
src/main.cpp |
Core bug fix for connection counting; integrated caching mechanism; refactored IP normalization |
src/core.cpp |
New file with extracted IP normalization and DNS lookup functions |
include/report_cache.hpp |
Cache implementation for tracking reported IP statistics |
include/public_api.hpp |
Public API declarations for testable core functions |
include/options.hpp |
Added new command-line options for cache control and sorting |
include/extensions.hpp |
Refactored trim functions to use string_view and modern C++ |
tests/report_cache_test.cpp |
Unit tests for cache functionality |
tests/main_extensions_test.cpp |
Unit tests for utility functions |
tests/end_to_end_cache_test.cpp |
End-to-end tests verifying cache behavior |
tests/CMakeLists.txt |
Test build configuration with GoogleTest integration |
CMakeLists.txt |
Build system updates for library extraction and coverage support |
scripts/coverage.sh |
Script for generating code coverage reports |
Dockerfile |
Docker image for running tests and coverage in CI |
.github/workflows/coverage.yml |
GitHub Actions workflow for automated coverage reporting |
.dockerignore |
Docker build optimizations |
README.md |
Updated documentation with new command-line options |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| string normalizeIp(string ip) { | ||
| const auto offset = ip.find("::ffff:"); | ||
| if (offset != string::npos) { | ||
| ip = ip.substr(offset + 7); | ||
| } | ||
| return ip; |
There was a problem hiding this comment.
The normalizeIp function takes its parameter by value (line 14), which causes an unnecessary copy of the string. Since the function modifies and returns the string, this is acceptable but could be optimized. However, changing this would affect the public API. Consider documenting this design choice or potentially offering an overload that takes a const reference if performance becomes a concern for large-scale usage.
| for (const auto& connection : connectionList) { | ||
| totalAcceptedConnections += connection.second.first; | ||
| totalClosedConnections += connection.second.second; | ||
|
|
||
| if (g_printIpStatistics) { | ||
| string lastSpacer; | ||
| string tmpString; | ||
| const auto offset = connection.first.find("::ffff:"); | ||
| if (offset != string::npos) { | ||
| tmpString = connection.first.substr(offset + 7); | ||
|
|
||
| tmpString = normalizeIp(connection.first); | ||
|
|
||
| if (g_strict && g_reportCache) { | ||
| IpStatsSnapshot snap; | ||
| snap.accepted = connection.second.first; | ||
| snap.closed = connection.second.second; | ||
| if (g_reportCache->hasUnchanged(tmpString, snap)) { | ||
| continue; | ||
| } | ||
| g_reportCache->update(tmpString, snap); | ||
| g_cacheDirty = true; | ||
| } |
There was a problem hiding this comment.
When strict mode is enabled and an IP is skipped because its stats haven't changed (line 571), the connection totals are still accumulated (lines 557-558). This creates an inconsistency where the total counters include skipped IPs while the table only shows changed IPs. Consider moving the counter accumulation after the cache check, or documenting this behavior if intentional.
| find . -name '*.gcda' -print0 | xargs -0 -n1 dirname | sort -u | while read -r dir; do | ||
| echo "Processing $dir" | ||
| # run gcov for all source files in the directory | ||
| (cd "$dir" && for f in $(find . -name '*.cpp' -o -name '*.c' 2>/dev/null) ; do true; done) |
There was a problem hiding this comment.
The gcov fallback loop (line 40) doesn't actually run gcov on the found files. The loop body only contains true, which does nothing. This should be replaced with an actual gcov command, such as gcov "$f" or similar, to generate coverage information.
| (cd "$dir" && for f in $(find . -name '*.cpp' -o -name '*.c' 2>/dev/null) ; do true; done) | |
| (cd "$dir" && find . \( -name '*.cpp' -o -name '*.c' \) -print0 2>/dev/null | while IFS= read -r -d '' f; do gcov "$f"; done) |
| inline string& trimStart(string& nonTrimmed, const string_view trimChars) { | ||
| nonTrimmed.erase(nonTrimmed.begin(), std::find_if(nonTrimmed.begin(), nonTrimmed.end(), [&trimChars](int ch) { return trimChars.find(static_cast<char>(ch)) == string_view::npos; })); | ||
|
|
||
| return nonTrimmed; | ||
| } |
There was a problem hiding this comment.
The comment for the trimChar parameter still references a single character and mentions a default value, but the function signature now uses trimChars (plural) of type string_view with no default value. The documentation should be updated to match the implementation.
| set(CMAKE_CXX_STANDARD 17) | ||
| set(CMAKE_CXX_STANDARD_REQUIRED True) | ||
|
|
||
| option(ENABLE_COVERAGE "Enable compilation flags for gcov coverage" ON) |
There was a problem hiding this comment.
The ENABLE_COVERAGE option is set to ON by default (line 8), which means coverage instrumentation is always enabled unless explicitly disabled. This will cause a performance penalty in production builds. Consider setting the default to OFF and enabling it only for development or CI builds.
| option(ENABLE_COVERAGE "Enable compilation flags for gcov coverage" ON) | |
| option(ENABLE_COVERAGE "Enable compilation flags for gcov coverage" OFF) |
| ${GTEST_MAIN_TARGET} | ||
| ${GMOCK_TARGET} | ||
| fmt | ||
| endlessh_core |
There was a problem hiding this comment.
The endlessh_core library is listed outside the PRIVATE scope in the target_link_libraries command. It should be moved inside the PRIVATE section on line 43 to maintain consistency with the other dependencies.
| endlessh_core | |
| endlessh_core |
| string host; | ||
| // By default the application runs without DNS lookups; keep behaviour | ||
| // identical to original implementation when lookups are disabled. | ||
| extern bool g_performDnsLookup; // declared in main.cpp |
There was a problem hiding this comment.
The extern declaration here is redundant and confusing. The global variable g_performDnsLookup is already defined in this file at line 12, and it's declared in the header file public_api.hpp at line 16. The extern declaration inside this function serves no purpose and may cause confusion about where the variable is actually defined.
| extern bool g_performDnsLookup; // declared in main.cpp |
| inline string& trimEnd(string& nonTrimmed, const string_view trimChars) { | ||
| nonTrimmed.erase(std::find_if(nonTrimmed.rbegin(), nonTrimmed.rend(), [&trimChars](int ch) { return trimChars.find(static_cast<char>(ch)) == string_view::npos; }).base(), nonTrimmed.end()); | ||
|
|
||
| return nonTrimmed; | ||
| } |
There was a problem hiding this comment.
The comment for the trimChar parameter still references a single character and mentions a default value, but the function signature now uses trimChars (plural) of type string_view with no default value. The documentation should be updated to match the implementation.
| * @return The trimmed string. | ||
| */ | ||
| inline string trim(const string& nonTrimmed, const string& trimChar) { return trimStart(trimEnd(nonTrimmed, trimChar), trimChar); } | ||
| inline string& trim(string& nonTrimmed, const string_view trimChars) { return trimStart(trimEnd(nonTrimmed, trimChars), trimChars); } |
There was a problem hiding this comment.
The comment for the trimChar parameter still references a single character and mentions a default value, but the function signature now uses trimChars (plural) of type string_view with no default value. The documentation should be updated to match the implementation.
| #include <string> | ||
|
|
||
| #include "extensions.hpp" | ||
| #include "public_api.hpp" | ||
|
|
||
| using std::string; |
There was a problem hiding this comment.
The vector type is used without including the necessary header or using declaration. While the extensions.hpp header likely includes <vector> and has a using declaration, it's better practice to include the header and declare the using directive explicitly in the test file for clarity and to avoid relying on transitive includes.
| #include <string> | |
| #include "extensions.hpp" | |
| #include "public_api.hpp" | |
| using std::string; | |
| #include <string> | |
| #include <vector> | |
| #include "extensions.hpp" | |
| #include "public_api.hpp" | |
| using std::string; | |
| using std::vector; |
Fixes #23.
Fixes issue where automated reports reported entries multiple times.