test(consensus): make path resolution independent of cwd (CI gate fix)

GitHub Actions CI (workflow 'Build All Platforms', run #29559727754)
flagged test-linux-unit and test-linux-sanitizers as failing. The CI
runs the test binary via 'ctest --output-on-failure' from build/, but
the consensus_safety_tests static-source grep tests called
readEntireFile('src/main.cpp') with paths resolved relative to CWD.

With CWD = build/, those paths did not exist; the tests failed with
'critical check !src.empty() has failed'. Same root cause for the
staking_tests::is_staking_safe_is_continuous_not_one_shot test which
opened 'src/miner.cpp' by raw __FILE__ slicing.

Specifically:
  consensus_safety_tests.cpp: 9 failures across
    convergence_rejects_below_hardened_checkpoint, hardened_checkpoint_init_is_startup_only,
    above_checkpoint_greatest_trust_wins, getheaders_recovers_via_genesis_when_locator_disjoint,
    getheaders_recovers_via_checkpoint_when_locator_has_it, reorg_guard_fails_closed_when_checkpoint_pointer_null,
    reorg_guard_offbyone_hardening, hardened_checkpoint_no_rogue_guard_in_other_files
  staking_tests.cpp: 1 failure in is_staking_safe_is_continuous_not_one_shot

Note: my pre-merge '280/280 tests pass' claim was based on running the
test binary directly from the repo root, where 'src/' resolves
trivially. ctest is the canonical CI invocation. This CI run was the
first time we exercised it.

Fix:
- Add findProjectRootFromHere(__FILE__) helper that:
  (1) prefers an absolute path in __FILE__ (/foo/bar/src/test/...),
  (2) falls back to a build-dir-relative anchor (./src/test/... or
      bare src/test/...) when cmake+ninja produces those,
  (3) defends against ctest's CWD=build/ by walking up from CWD
      looking for the canonical src/checkpoints.cpp sentinel.
- Apply uniformly in consensus_safety_tests.cpp (helper + 1 rogue-guard
  walker that builds project-root-relative paths before comparing
  against allowed_files) and staking_tests.cpp (mirrored helper).
- Strict-mode BOOST_REQUIRE_MESSAGE failure paths now include the
  resolved path so the next person debugging this hits the issue
  immediately.

Verified: 'cd build && ctest --output-on-failure' now reports 0
failures across all 4 ctest projects (triangles_unit_tests,
chaindb_equivalence_tests, snapshotnet_tests, chaindb_runtime_tests).
Direct 'test_triangles' invocation still works for ad-hoc checks.
This commit is contained in:
Sami Ahmed
2026-07-17 00:59:45 -07:00
parent 021d4bf093
commit 49cf7ab2c2
2 changed files with 133 additions and 25 deletions
+45 -6
View File
@@ -3,12 +3,48 @@
#include <boost/test/unit_test.hpp>
#include <filesystem>
#include <vector>
#include "../main.h"
#include "../kernel.h"
extern unsigned int nStakeMinAge;
extern unsigned int nStakeMaxAge;
// Resolve the project root from this test file's __FILE__.
// See consensus_safety_tests.cpp::findProjectRootFromHere for the full
// rationale — the version here is kept in lock-step so that local
// `ctest --output-on-failure` runs from build/ succeed.
static std::string findProjectRootFromHere_staking(const std::string& here)
{
namespace fs = std::filesystem;
std::string h = here;
while (h.size() >= 2 && h[0] == '.' && h[1] == '/') h.erase(0, 2);
size_t abs_pos = h.rfind("/src/test/");
if (abs_pos != std::string::npos) {
std::string root = h.substr(0, abs_pos);
if (!root.empty()) return root + "/";
}
size_t rel_pos = h.rfind("src/test/");
if (rel_pos != std::string::npos) {
std::string prefix = h.substr(0, rel_pos);
fs::path candidate;
if (prefix.empty()) candidate = fs::current_path();
else candidate = fs::path(prefix);
if (fs::exists(candidate / "src" / "checkpoints.cpp"))
return candidate.string() + "/";
}
fs::path cur = fs::current_path();
for (int i = 0; i < 8; ++i) {
if (fs::exists(cur / "src" / "checkpoints.cpp"))
return cur.string() + "/";
if (cur == cur.root_path()) break;
cur = cur.parent_path();
}
return "./";
}
BOOST_AUTO_TEST_SUITE(staking_tests)
// --- GetWeight: coin age weight calculation ---
@@ -354,14 +390,17 @@ BOOST_AUTO_TEST_CASE(is_staking_safe_is_continuous_not_one_shot)
// wait loop. Post-fix must NOT have the fTryToSync flag at all.
//
// Use __FILE__ to find the repo root so the path resolves regardless
// of the build directory or test runner cwd.
std::string here = __FILE__;
size_t pos = here.rfind("/src/test/");
BOOST_REQUIRE(pos != std::string::npos);
std::string miner_src_path = here.substr(0, pos) + "/src/miner.cpp";
// of the build directory or test runner cwd. The resolver tolerates
// both absolute paths and the bare-rel or "./"-rel forms cmake+ninja
// sometimes bake in, and falls back to walking up from CWD looking
// for src/checkpoints.cpp.
std::string root = findProjectRootFromHere_staking(__FILE__);
std::string miner_src_path = root + "src/miner.cpp";
FILE* f = fopen(miner_src_path.c_str(), "r");
BOOST_REQUIRE(f != nullptr);
BOOST_REQUIRE_MESSAGE(f != nullptr,
"Could not open '" + miner_src_path + "' — repository root resolution is "
"broken; ctest from build/ would also fail.");
fseek(f, 0, SEEK_END);
long nSize = ftell(f);
fseek(f, 0, SEEK_SET);