Skip to content

Commit 2fdc666

Browse files
committed
Add coverity and more binary fuse filter tests
1 parent cf79fa1 commit 2fdc666

File tree

5 files changed

+305
-16
lines changed

5 files changed

+305
-16
lines changed

lib/binaryfusefilter.h

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@
2222

2323
typedef std::array<uint8_t, crypto_shorthash_KEYBYTES> binary_fuse_seed_t;
2424

25+
#ifdef BUILD_TESTS
26+
27+
#include "test/CovMark.h"
28+
29+
// Test-only flags: force specific rare paths in populate() so tests can
30+
// exercise them without needing pathological hash distributions.
31+
inline bool gBinaryFuseForcePopulateError = false;
32+
inline bool gBinaryFuseForcePeelingFailure = false;
33+
#endif
34+
2535
#ifndef XOR_MAX_ITERATIONS
2636
#define XOR_MAX_ITERATIONS \
2737
1000000 // probability of success should always be > 0.5, so with this many
@@ -256,14 +266,14 @@ template <typename T> class binary_fuse_t
256266
return h;
257267
}
258268

259-
// Construct the filter, returns true on success, false on failure.
260-
// The algorithm fails when there is insufficient memory.
269+
// Construct the filter. Throws std::runtime_error if population fails
270+
// after max iterations (practically impossible).
261271
// For best performance, the caller should ensure that there are not
262272
// too many duplicated keys.
263-
// While highly improbable, it is possible that the population fails, at
264-
// which point the seed must be rotated. keys will be sorted and duplicates
265-
// removed if any duplicate keys exist
266-
[[nodiscard]] bool
273+
// While highly improbable, it is possible that an iteration fails, at
274+
// which point the seed is rotated and retried. keys will be sorted and
275+
// duplicates removed if any duplicate keys exist
276+
void
267277
populate(std::vector<uint64_t>& keys, binary_fuse_seed_t rngSeed)
268278
{
269279
ZoneScoped;
@@ -295,6 +305,9 @@ template <typename T> class binary_fuse_t
295305
std::vector<uint32_t> startPos(block);
296306
std::vector<uint32_t> h012(5);
297307

308+
// Non-zero sentinel past the last valid slot. The placement loop
309+
// treats 0 as empty; this prevents startPos from advancing past
310+
// the end of the array.
298311
reverseOrder.at(size) = 1;
299312
for (int loop = 0; true; ++loop)
300313
{
@@ -303,7 +316,8 @@ template <typename T> class binary_fuse_t
303316
// The probability of this happening is lower than the
304317
// the cosmic-ray probability (i.e., a cosmic ray corrupts your
305318
// system).
306-
return false;
319+
throw std::runtime_error(
320+
"BinaryFuseFilter failed to populate after max iterations");
307321
}
308322

309323
for (uint32_t i = 0; i < block; i++)
@@ -363,9 +377,16 @@ template <typename T> class binary_fuse_t
363377
error = (t2count.at(h1) < 4) ? 1 : error;
364378
error = (t2count.at(h2) < 4) ? 1 : error;
365379
}
380+
#ifdef BUILD_TESTS
381+
if (!error && gBinaryFuseForcePopulateError && loop == 0)
382+
{
383+
error = 1;
384+
gBinaryFuseForcePopulateError = false;
385+
}
386+
#endif
366387
if (error)
367388
{
368-
// Reset everything except the sentinel at reverseOrder[size]
389+
COVMARK_HIT(BINARY_FUSE_POPULATE_ERROR_RETRY);
369390
std::fill_n(reverseOrder.begin(), size, 0);
370391
std::fill(t2count.begin(), t2count.end(), 0);
371392
std::fill(t2hash.begin(), t2hash.end(), 0);
@@ -420,6 +441,14 @@ template <typename T> class binary_fuse_t
420441
t2hash.at(other_index2) ^= hash;
421442
}
422443
}
444+
#ifdef BUILD_TESTS
445+
if (gBinaryFuseForcePeelingFailure && loop == 0)
446+
{
447+
stacksize = 0;
448+
duplicates = 0;
449+
gBinaryFuseForcePeelingFailure = false;
450+
}
451+
#endif
423452
if (stacksize + duplicates == size)
424453
{
425454
// success
@@ -428,12 +457,18 @@ template <typename T> class binary_fuse_t
428457
}
429458
else if (duplicates > 0)
430459
{
460+
COVMARK_HIT(BINARY_FUSE_DUPLICATE_REMOVAL);
431461
// Sort keys and remove duplicates
432462
std::sort(keys.begin(), keys.end());
433463
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
434464
size = keys.size();
465+
466+
// Size may hav decreased, so make sure we write a new sentinel
467+
// value.
468+
reverseOrder.at(size) = 1;
435469
}
436470

471+
COVMARK_HIT(BINARY_FUSE_POPULATE_PEELING_FAILURE_RETRY);
437472
// Reset everything except for the last entry in reverseOrder
438473
std::fill_n(reverseOrder.begin(), size, 0);
439474
std::fill(t2count.begin(), t2count.end(), 0);
@@ -458,8 +493,6 @@ template <typename T> class binary_fuse_t
458493
Fingerprints.at(h012.at(found)) = xor2 ^ Fingerprints.at(h012.at(found + 1)) ^
459494
Fingerprints.at(h012.at(found + 2));
460495
}
461-
462-
return true;
463496
}
464497

465498
public:
@@ -498,10 +531,7 @@ template <typename T> class binary_fuse_t
498531
SegmentCountLength = SegmentCount * SegmentLength;
499532
Fingerprints.resize(ArrayLength);
500533

501-
if (!populate(keys, rngSeed))
502-
{
503-
throw std::runtime_error("BinaryFuseFilter failed to populate");
504-
}
534+
populate(keys, rngSeed);
505535
}
506536

507537
explicit binary_fuse_t(stellar::SerializedBinaryFuseFilter const& xdrFilter)

src/bucket/DiskIndex.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,6 @@ DiskIndex<BucketT>::DiskIndex(BucketManager& bm,
256256
if (keyHashes.size() > 1)
257257
{
258258
mData.filter = std::make_unique<BinaryFuseFilter16>(keyHashes, seed);
259-
// Population failure is probabilistic is very, very unlikely.
260-
releaseAssertOrThrow(mData.filter);
261259
}
262260

263261
CLOG_DEBUG(Bucket, "Indexed {} positions in {}", mData.keysToOffset.size(),

src/test/CovMark.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright 2026 Stellar Development Foundation and contributors. Licensed
2+
// under the Apache License, Version 2.0. See the COPYING file at the root
3+
// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0
4+
5+
#include "test/CovMark.h"
6+
7+
#ifdef BUILD_TESTS
8+
namespace stellar
9+
{
10+
CovMarks gCovMarks;
11+
}
12+
#endif

src/test/CovMark.h

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright 2026 Stellar Development Foundation and contributors. Licensed
2+
// under the Apache License, Version 2.0. See the COPYING file at the root
3+
// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0
4+
5+
#pragma once
6+
7+
// Coverage marks: a lightweight mechanism for tests to assert that specific
8+
// internal code paths were actually executed. Inspired by the "coverage marks"
9+
// pattern from Ferrous Systems.
10+
//
11+
// Usage in production code:
12+
// COVMARK_HIT(SOME_RARE_PATH);
13+
//
14+
// Usage in test code:
15+
// COVMARK_CHECK_HIT_IN_CURR_SCOPE(SOME_RARE_PATH);
16+
// // ... code that should trigger SOME_RARE_PATH ...
17+
//
18+
// The check-hit guard records the counter on construction and verifies it
19+
// increased by scope exit. If the marked path was not hit, the test fails.
20+
21+
#include <array>
22+
#include <atomic>
23+
#include <cstdint>
24+
#include <exception>
25+
#include <stdexcept>
26+
27+
#ifdef BUILD_TESTS
28+
#include <fmt/format.h>
29+
#endif
30+
31+
namespace stellar
32+
{
33+
34+
// Each coverage mark is an enum value. Add new marks before COVMARK_COUNT.
35+
enum CovMark : std::size_t
36+
{
37+
BINARY_FUSE_POPULATE_ERROR_RETRY = 0,
38+
BINARY_FUSE_POPULATE_PEELING_FAILURE_RETRY,
39+
BINARY_FUSE_DUPLICATE_REMOVAL,
40+
COVMARK_COUNT // must be last
41+
};
42+
43+
#ifdef BUILD_TESTS
44+
45+
class CovMarks
46+
{
47+
std::array<std::atomic<std::uint64_t>, CovMark::COVMARK_COUNT> mCounters{};
48+
49+
public:
50+
void
51+
hit(CovMark mark)
52+
{
53+
mCounters[mark].fetch_add(1, std::memory_order_relaxed);
54+
}
55+
56+
std::uint64_t
57+
get(CovMark mark) const
58+
{
59+
return mCounters[mark].load(std::memory_order_relaxed);
60+
}
61+
62+
void
63+
reset()
64+
{
65+
for (auto& c : mCounters)
66+
{
67+
c.store(0, std::memory_order_relaxed);
68+
}
69+
}
70+
};
71+
72+
extern CovMarks gCovMarks;
73+
74+
class CovMarkGuard
75+
{
76+
CovMark mMark;
77+
std::uint64_t mValueOnEntry;
78+
char const* mFile;
79+
int mLine;
80+
char const* mName;
81+
82+
public:
83+
CovMarkGuard(CovMark mark, char const* file, int line, char const* name)
84+
: mMark(mark)
85+
, mValueOnEntry(gCovMarks.get(mark))
86+
, mFile(file)
87+
, mLine(line)
88+
, mName(name)
89+
{
90+
}
91+
92+
~CovMarkGuard() noexcept(false)
93+
{
94+
if (std::uncaught_exceptions() == 0)
95+
{
96+
auto valueOnExit = gCovMarks.get(mMark);
97+
if (valueOnExit <= mValueOnEntry)
98+
{
99+
throw std::runtime_error(
100+
fmt::format("{}:{}: coverage mark '{}' was not hit during "
101+
"this scope",
102+
mFile, mLine, mName));
103+
}
104+
}
105+
}
106+
};
107+
108+
#define COVMARK_HIT(covmark) \
109+
::stellar::gCovMarks.hit(::stellar::CovMark::covmark)
110+
111+
#define COVMARK_CHECK_HIT_IN_CURR_SCOPE(covmark) \
112+
::stellar::CovMarkGuard _covMarkGuard_##covmark( \
113+
::stellar::CovMark::covmark, __FILE__, __LINE__, #covmark)
114+
115+
#else // !BUILD_TESTS
116+
117+
#define COVMARK_HIT(covmark) ((void)0)
118+
#define COVMARK_CHECK_HIT_IN_CURR_SCOPE(covmark) ((void)0)
119+
120+
#endif // BUILD_TESTS
121+
}

0 commit comments

Comments
 (0)