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:
@@ -413,20 +413,72 @@ BOOST_AUTO_TEST_CASE(target_spacing_immutable)
|
|||||||
// 4. There is no longer a 10% trust hysteresis check.
|
// 4. There is no longer a 10% trust hysteresis check.
|
||||||
// Helper: resolve the repository root from the test file's __FILE__
|
// Helper: resolve the repository root from the test file's __FILE__
|
||||||
// so the static-source tests below don't depend on the caller's cwd.
|
// so the static-source tests below don't depend on the caller's cwd.
|
||||||
// We assume the test file lives at <root>/src/test/<this>.cpp.
|
//
|
||||||
|
// __FILE__ resolution varies by build system:
|
||||||
|
// - Absolute path: "/foo/bar/src/test/foo.cpp" (most cmake configs)
|
||||||
|
// - Build-dir relative: "./src/test/foo.cpp" (cmake + ninja often)
|
||||||
|
// - Repo-relative: "src/test/foo.cpp" (we've seen this too;
|
||||||
|
// strips to nothing on `rfind("src/test/")` so we must NOT take
|
||||||
|
// that as the project root, because ctest runs from build/,
|
||||||
|
// not the repo root).
|
||||||
|
//
|
||||||
|
// Resolution strategy: take "everything strictly before src/test/"
|
||||||
|
// if that prefix itself points to a directory (or to the filesystem
|
||||||
|
// root). Otherwise (bare "src/test/foo.cpp"), fall back to walking
|
||||||
|
// up from CWD looking for the canonical src/checkpoints.cpp sentinel.
|
||||||
|
// This always works because ctest sets CWD to the build dir, and we
|
||||||
|
// can find the repo root by walking up until we hit one containing
|
||||||
|
// src/.
|
||||||
|
static std::string findProjectRootFromHere(const std::string& here)
|
||||||
|
{
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
std::string h = here;
|
||||||
|
|
||||||
|
// Strip any leading "./" so the search anchors line up.
|
||||||
|
while (h.size() >= 2 && h[0] == '.' && h[1] == '/') h.erase(0, 2);
|
||||||
|
|
||||||
|
// Anchor 1: "/src/test/" — absolute path form.
|
||||||
|
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 + "/";
|
||||||
|
}
|
||||||
|
// Anchor 2: "src/test/" (relative path, no leading slash).
|
||||||
|
// Only accept this as the project root if the prefix, joined
|
||||||
|
// with the cwd, actually exists as a directory containing a
|
||||||
|
// src/ subtree. Otherwise we have a bare relative path with no
|
||||||
|
// prefix and ctest's CWD is build/, so we must walk up.
|
||||||
|
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() + "/";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: walk up from CWD looking for the canonical src/ sentinel.
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
// Last-resort fallback: cwd + "src/"
|
||||||
|
return "./";
|
||||||
|
}
|
||||||
|
|
||||||
static std::string readEntireFile(const char* relToSrc)
|
static std::string readEntireFile(const char* relToSrc)
|
||||||
{
|
{
|
||||||
// __FILE__ resolves to an absolute path under typical compilers;
|
static const std::string root = findProjectRootFromHere(__FILE__);
|
||||||
// fall back to a CWD-relative path if it doesn't.
|
std::string full = root + relToSrc;
|
||||||
std::string here = __FILE__;
|
|
||||||
size_t pos = here.rfind("/src/test/");
|
|
||||||
std::string root;
|
|
||||||
if (pos != std::string::npos)
|
|
||||||
root = here.substr(0, pos);
|
|
||||||
else
|
|
||||||
root = ".";
|
|
||||||
|
|
||||||
std::string full = root + "/" + relToSrc;
|
|
||||||
FILE* f = fopen(full.c_str(), "r");
|
FILE* f = fopen(full.c_str(), "r");
|
||||||
if (!f)
|
if (!f)
|
||||||
return std::string();
|
return std::string();
|
||||||
@@ -713,12 +765,17 @@ BOOST_AUTO_TEST_CASE(hardened_checkpoint_no_rogue_guard_in_other_files)
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Walk src/ non-recursively into subdirs except the excluded ones.
|
// Resolve src/ from the project root, not the current working dir.
|
||||||
|
// Under ctest the CWD is <repo>/build, but the source tree is at
|
||||||
|
// <repo>/src — use the same root resolver as readEntireFile().
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
fs::path src_root = "src";
|
const std::string projectRoot = findProjectRootFromHere(__FILE__);
|
||||||
|
fs::path src_root = projectRoot + "src";
|
||||||
if (!fs::exists(src_root))
|
if (!fs::exists(src_root))
|
||||||
{
|
{
|
||||||
BOOST_FAIL("src/ directory not found at test runtime; cannot walk for rogue-guard detection");
|
BOOST_FAIL("src/ directory not found at test runtime at resolved path '"
|
||||||
|
+ src_root.string() + "'. Project-root resolution is broken — "
|
||||||
|
"fix findProjectRootFromHere() in this file before trusting the test.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,6 +783,17 @@ BOOST_AUTO_TEST_CASE(hardened_checkpoint_no_rogue_guard_in_other_files)
|
|||||||
std::string firstOffender;
|
std::string firstOffender;
|
||||||
std::vector<std::string> scanned;
|
std::vector<std::string> scanned;
|
||||||
|
|
||||||
|
// Build a relative path anchored at <projectRoot>, so it looks
|
||||||
|
// like "src/main.cpp" regardless of whether the walker entered
|
||||||
|
// via an absolute or a relative starting point. This matches
|
||||||
|
// the relative style used in allowed_files[] below.
|
||||||
|
auto to_rel = [&](const fs::path& p) -> std::string {
|
||||||
|
std::string s = p.string();
|
||||||
|
if (!projectRoot.empty() && s.compare(0, projectRoot.size(), projectRoot) == 0)
|
||||||
|
s.erase(0, projectRoot.size());
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
|
||||||
// Recursive walk, with excluded-dir pruning.
|
// Recursive walk, with excluded-dir pruning.
|
||||||
std::function<void(const fs::path&)> walk = [&](const fs::path& dir) {
|
std::function<void(const fs::path&)> walk = [&](const fs::path& dir) {
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
@@ -735,9 +803,10 @@ BOOST_AUTO_TEST_CASE(hardened_checkpoint_no_rogue_guard_in_other_files)
|
|||||||
{
|
{
|
||||||
const auto& entry = *it;
|
const auto& entry = *it;
|
||||||
std::string path = entry.path().string();
|
std::string path = entry.path().string();
|
||||||
|
std::string rel = to_rel(entry.path());
|
||||||
if (entry.is_directory(ec))
|
if (entry.is_directory(ec))
|
||||||
{
|
{
|
||||||
if (!is_excluded_dir(path))
|
if (!is_excluded_dir(path) && !is_excluded_dir(rel))
|
||||||
walk(entry.path());
|
walk(entry.path());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -745,16 +814,16 @@ BOOST_AUTO_TEST_CASE(hardened_checkpoint_no_rogue_guard_in_other_files)
|
|||||||
// Only .cpp and .h files.
|
// Only .cpp and .h files.
|
||||||
std::string ext = entry.path().extension().string();
|
std::string ext = entry.path().extension().string();
|
||||||
if (ext != ".cpp" && ext != ".h") continue;
|
if (ext != ".cpp" && ext != ".h") continue;
|
||||||
if (is_allowed(path)) continue;
|
if (is_allowed(rel)) continue;
|
||||||
// Read and check for the literal variable reference.
|
// Read and check for the literal variable reference.
|
||||||
std::ifstream f(entry.path());
|
std::ifstream f(entry.path());
|
||||||
if (!f.good()) continue;
|
if (!f.good()) continue;
|
||||||
std::stringstream ss; ss << f.rdbuf();
|
std::stringstream ss; ss << f.rdbuf();
|
||||||
const std::string& contents = ss.str();
|
const std::string& contents = ss.str();
|
||||||
scanned.push_back(path);
|
scanned.push_back(rel);
|
||||||
if (contents.find("pindexLastHardenedCheckpoint") != std::string::npos)
|
if (contents.find("pindexLastHardenedCheckpoint") != std::string::npos)
|
||||||
{
|
{
|
||||||
if (firstOffender.empty()) firstOffender = path;
|
if (firstOffender.empty()) firstOffender = rel;
|
||||||
++nFailures;
|
++nFailures;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,48 @@
|
|||||||
|
|
||||||
#include <boost/test/unit_test.hpp>
|
#include <boost/test/unit_test.hpp>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "../main.h"
|
#include "../main.h"
|
||||||
#include "../kernel.h"
|
#include "../kernel.h"
|
||||||
|
|
||||||
extern unsigned int nStakeMinAge;
|
extern unsigned int nStakeMinAge;
|
||||||
extern unsigned int nStakeMaxAge;
|
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)
|
BOOST_AUTO_TEST_SUITE(staking_tests)
|
||||||
|
|
||||||
// --- GetWeight: coin age weight calculation ---
|
// --- 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.
|
// wait loop. Post-fix must NOT have the fTryToSync flag at all.
|
||||||
//
|
//
|
||||||
// Use __FILE__ to find the repo root so the path resolves regardless
|
// Use __FILE__ to find the repo root so the path resolves regardless
|
||||||
// of the build directory or test runner cwd.
|
// of the build directory or test runner cwd. The resolver tolerates
|
||||||
std::string here = __FILE__;
|
// both absolute paths and the bare-rel or "./"-rel forms cmake+ninja
|
||||||
size_t pos = here.rfind("/src/test/");
|
// sometimes bake in, and falls back to walking up from CWD looking
|
||||||
BOOST_REQUIRE(pos != std::string::npos);
|
// for src/checkpoints.cpp.
|
||||||
std::string miner_src_path = here.substr(0, pos) + "/src/miner.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");
|
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);
|
fseek(f, 0, SEEK_END);
|
||||||
long nSize = ftell(f);
|
long nSize = ftell(f);
|
||||||
fseek(f, 0, SEEK_SET);
|
fseek(f, 0, SEEK_SET);
|
||||||
|
|||||||
Reference in New Issue
Block a user