From 996ef1386eee6ea6d9078485e56018b2cc9501e5 Mon Sep 17 00:00:00 2001 From: KeviM Date: Fri, 25 Sep 2026 14:31:46 -0700 Subject: [PATCH 1/2] test: skip dotfiles in golden coverage and zero counters before counting --- tests/support/allocation_counter.cpp | 8 ++++++-- tests/test_golden.cpp | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/support/allocation_counter.cpp b/tests/support/allocation_counter.cpp index e5c9174..abb70b9 100644 --- a/tests/support/allocation_counter.cpp +++ b/tests/support/allocation_counter.cpp @@ -20,7 +20,8 @@ namespace { struct Counters { - std::atomic active{false}; + std::atomic owned{false}; // a probe exists + std::atomic active{false}; // allocations are being counted std::atomic allocations{0}; std::atomic bytes{0}; std::atomic live{0}; @@ -118,17 +119,20 @@ std::size_t to_size(std::align_val_t alignment) noexcept { namespace spc::test { AllocationProbe::AllocationProbe() { - if (counters.active.exchange(true)) { + if (counters.owned.exchange(true)) { throw std::logic_error("another AllocationProbe is active"); } + // Zero before counting starts, so no allocation is counted and then wiped. counters.allocations.store(0); counters.bytes.store(0); counters.live.store(0); counters.peak.store(0); + counters.active.store(true); } AllocationProbe::~AllocationProbe() { counters.active.store(false); + counters.owned.store(false); } AllocationStats AllocationProbe::stats() const noexcept { diff --git a/tests/test_golden.cpp b/tests/test_golden.cpp index 8e5b40f..55ddf31 100644 --- a/tests/test_golden.cpp +++ b/tests/test_golden.cpp @@ -191,7 +191,8 @@ TEST(GoldenCoverage, EveryFixtureIsParsedOrListedAsNotParsed) { for (const std::filesystem::directory_entry& entry : std::filesystem::directory_iterator(SPC_FIXTURES_DIR)) { const std::string name = entry.path().filename().string(); - if (name == "README.md" || name == "SHA256SUMS") { + // Dotfiles such as a Finder .DS_Store are not fixtures. + if (name == "README.md" || name == "SHA256SUMS" || name.starts_with('.')) { continue; } EXPECT_TRUE(has_case(name) || std::ranges::find(kNotParsed, name) != kNotParsed.end()) From 86de457ac191513d44859e723485f9564eca2a55 Mon Sep 17 00:00:00 2001 From: KeviM Date: Fri, 25 Sep 2026 14:51:49 -0700 Subject: [PATCH 2/2] test: harden the allocation counter, benchmarks, and golden output Export the replacement operator new and delete, so LTO cannot internalize them and leave allocations inside the standard library uncounted. The benchmarks now check at startup that the counter sees an allocation made directly and one made inside the standard library, and exit with the likely cause when it does not, as under Valgrind. Configuring the benchmarks with a sanitizer now fails, since both replace operator new. The counter test covers all eight new forms and twelve delete forms, and the standard-library test uses std::runtime_error, whose message copy is a library call on libc++ and libstdc++. GCC inlines std::string growth. make bench calls cmake directly, so CMAKE_ARGS keeps its quoting. Golden diffs escape their excerpts, so a stray carriage return shows, and each golden renderer destructures its model, so a new field breaks the build until it is printed. --- CMakeLists.txt | 4 + CONTRIBUTING.md | 4 + Makefile | 5 +- benchmarks/benchmarks.cpp | 33 +++++++++ tests/support/allocation_counter.cpp | 58 +++++++++------ tests/support/payload_text.hpp | 107 +++++++++++++++------------ tests/test_allocation_counter.cpp | 73 +++++++++++------- tests/test_golden.cpp | 7 +- 8 files changed, 191 insertions(+), 100 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ae0cf63..f996053 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,6 +77,10 @@ endif() if((SPC_ENABLE_SANITIZERS OR SPC_ENABLE_THREAD_SANITIZER OR SPC_ENABLE_COVERAGE) AND MSVC) message(FATAL_ERROR "Sanitizer and coverage options support GCC and Clang only") endif() +if(SPC_BUILD_BENCHMARKS AND (SPC_ENABLE_SANITIZERS OR SPC_ENABLE_THREAD_SANITIZER)) + message(FATAL_ERROR "SPC_BUILD_BENCHMARKS cannot be combined with sanitizers: the " + "benchmarks replace operator new and delete, as the sanitizer runtimes do") +endif() if(SPC_ENABLE_SANITIZERS) add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer) add_link_options(-fsanitize=address,undefined) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 70a4b99..752bd18 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -138,6 +138,10 @@ python3 build-bench/_deps/benchmark-src/tools/compare.py benchmarks \ build-bench/main.json build-bench/branch.json ``` +Under Valgrind, pass `--soname-synonyms=somalloc=nouserintercepts`, or +Valgrind's own operator new replaces the counting one and the suite exits +with an error. + ## Pull requests - Branch from `main` and open a pull request against it. Every CI job must diff --git a/Makefile b/Makefile index f2e5dfa..1c9473f 100644 --- a/Makefile +++ b/Makefile @@ -54,8 +54,9 @@ tidy: # Release build in build-bench, without tests and examples. bench: - @$(MAKE) --no-print-directory build BUILD_DIR=build-bench \ - CMAKE_ARGS="-DSPC_BUILD_BENCHMARKS=ON -DSPC_BUILD_TESTS=OFF -DSPC_BUILD_EXAMPLES=OFF $(CMAKE_ARGS)" + @$(CMAKE) -S . -B build-bench -DCMAKE_BUILD_TYPE=Release -DSPC_BUILD_BENCHMARKS=ON \ + -DSPC_BUILD_TESTS=OFF -DSPC_BUILD_EXAMPLES=OFF $(CMAKE_ARGS) + @$(CMAKE) --build build-bench -j$(NPROC) @./build-bench/benchmarks/spc_benchmarks $(BENCH_ARGS) coverage: diff --git a/benchmarks/benchmarks.cpp b/benchmarks/benchmarks.cpp index 3c4d71b..ac72601 100644 --- a/benchmarks/benchmarks.cpp +++ b/benchmarks/benchmarks.cpp @@ -21,11 +21,14 @@ #include #include #include +#include #include #include #include #include +#include #include +#include #include #include #include @@ -295,10 +298,40 @@ void register_benchmarks() { }); } +/// Whether the allocation counter sees an operator new call made here and one +/// made inside the standard library. When it does not, the heap counters would +/// read low without failing, so this prints the likely cause. +bool allocation_counter_works() { + const spc::test::AllocationProbe probe; + void* block = ::operator new(64); + benchmark::DoNotOptimize(block); + ::operator delete(block); + const std::uint64_t direct = probe.stats().allocations; + const std::runtime_error error("a message the standard library copies to the heap"); + benchmark::DoNotOptimize(error.what()); + const std::uint64_t library = probe.stats().allocations - direct; + if (direct != 1) { + std::fputs("The allocation counter misses operator new calls. Under Valgrind, run with " + "--soname-synonyms=somalloc=nouserintercepts.\n", + stderr); + return false; + } + if (library == 0) { + std::fputs("The allocation counter misses allocations made inside the standard library, " + "which happens when LTO internalizes the replacement operator new.\n", + stderr); + return false; + } + return true; +} + } // namespace int main(int argc, char** argv) { benchmark::MaybeReenterWithoutASLR(argc, argv); + if (!allocation_counter_works()) { + return 1; + } register_benchmarks(); benchmark::Initialize(&argc, argv); if (benchmark::ReportUnrecognizedArguments(argc, argv)) { diff --git a/tests/support/allocation_counter.cpp b/tests/support/allocation_counter.cpp index abb70b9..a086dfe 100644 --- a/tests/support/allocation_counter.cpp +++ b/tests/support/allocation_counter.cpp @@ -146,86 +146,96 @@ AllocationStats AllocationProbe::stats() const noexcept { } // namespace spc::test -void* operator new(std::size_t size) { +// LTO may internalize the replacements (ld64 does), and then allocations made +// inside a shared standard library bypass them. This keeps them exported. +#if defined(__GNUC__) +#define SPC_REPLACEMENT __attribute__((used, visibility("default"))) +#else +#define SPC_REPLACEMENT +#endif + +SPC_REPLACEMENT void* operator new(std::size_t size) { return allocate(size, 0); } -void* operator new[](std::size_t size) { +SPC_REPLACEMENT void* operator new[](std::size_t size) { return allocate(size, 0); } -void* operator new(std::size_t size, std::align_val_t alignment) { +SPC_REPLACEMENT void* operator new(std::size_t size, std::align_val_t alignment) { return allocate(size, to_size(alignment)); } -void* operator new[](std::size_t size, std::align_val_t alignment) { +SPC_REPLACEMENT void* operator new[](std::size_t size, std::align_val_t alignment) { return allocate(size, to_size(alignment)); } -void* operator new(std::size_t size, const std::nothrow_t& /*tag*/) noexcept { +SPC_REPLACEMENT void* operator new(std::size_t size, const std::nothrow_t& /*tag*/) noexcept { return allocate_nothrow(size, 0); } -void* operator new[](std::size_t size, const std::nothrow_t& /*tag*/) noexcept { +SPC_REPLACEMENT void* operator new[](std::size_t size, const std::nothrow_t& /*tag*/) noexcept { return allocate_nothrow(size, 0); } -void* operator new(std::size_t size, std::align_val_t alignment, - const std::nothrow_t& /*tag*/) noexcept { +SPC_REPLACEMENT void* operator new(std::size_t size, std::align_val_t alignment, + const std::nothrow_t& /*tag*/) noexcept { return allocate_nothrow(size, to_size(alignment)); } -void* operator new[](std::size_t size, std::align_val_t alignment, - const std::nothrow_t& /*tag*/) noexcept { +SPC_REPLACEMENT void* operator new[](std::size_t size, std::align_val_t alignment, + const std::nothrow_t& /*tag*/) noexcept { return allocate_nothrow(size, to_size(alignment)); } -void operator delete(void* ptr) noexcept { +SPC_REPLACEMENT void operator delete(void* ptr) noexcept { deallocate(ptr, 0); } -void operator delete[](void* ptr) noexcept { +SPC_REPLACEMENT void operator delete[](void* ptr) noexcept { deallocate(ptr, 0); } -void operator delete(void* ptr, std::size_t /*size*/) noexcept { +SPC_REPLACEMENT void operator delete(void* ptr, std::size_t /*size*/) noexcept { deallocate(ptr, 0); } -void operator delete[](void* ptr, std::size_t /*size*/) noexcept { +SPC_REPLACEMENT void operator delete[](void* ptr, std::size_t /*size*/) noexcept { deallocate(ptr, 0); } -void operator delete(void* ptr, std::align_val_t alignment) noexcept { +SPC_REPLACEMENT void operator delete(void* ptr, std::align_val_t alignment) noexcept { deallocate(ptr, to_size(alignment)); } -void operator delete[](void* ptr, std::align_val_t alignment) noexcept { +SPC_REPLACEMENT void operator delete[](void* ptr, std::align_val_t alignment) noexcept { deallocate(ptr, to_size(alignment)); } -void operator delete(void* ptr, std::size_t /*size*/, std::align_val_t alignment) noexcept { +SPC_REPLACEMENT void operator delete(void* ptr, std::size_t /*size*/, + std::align_val_t alignment) noexcept { deallocate(ptr, to_size(alignment)); } -void operator delete[](void* ptr, std::size_t /*size*/, std::align_val_t alignment) noexcept { +SPC_REPLACEMENT void operator delete[](void* ptr, std::size_t /*size*/, + std::align_val_t alignment) noexcept { deallocate(ptr, to_size(alignment)); } -void operator delete(void* ptr, const std::nothrow_t& /*tag*/) noexcept { +SPC_REPLACEMENT void operator delete(void* ptr, const std::nothrow_t& /*tag*/) noexcept { deallocate(ptr, 0); } -void operator delete[](void* ptr, const std::nothrow_t& /*tag*/) noexcept { +SPC_REPLACEMENT void operator delete[](void* ptr, const std::nothrow_t& /*tag*/) noexcept { deallocate(ptr, 0); } -void operator delete(void* ptr, std::align_val_t alignment, - const std::nothrow_t& /*tag*/) noexcept { +SPC_REPLACEMENT void operator delete(void* ptr, std::align_val_t alignment, + const std::nothrow_t& /*tag*/) noexcept { deallocate(ptr, to_size(alignment)); } -void operator delete[](void* ptr, std::align_val_t alignment, - const std::nothrow_t& /*tag*/) noexcept { +SPC_REPLACEMENT void operator delete[](void* ptr, std::align_val_t alignment, + const std::nothrow_t& /*tag*/) noexcept { deallocate(ptr, to_size(alignment)); } diff --git a/tests/support/payload_text.hpp b/tests/support/payload_text.hpp index 88dae98..3ac42dd 100644 --- a/tests/support/payload_text.hpp +++ b/tests/support/payload_text.hpp @@ -5,6 +5,9 @@ /// declaration order and its rings last. The text is the same on every /// standard library: strings are quoted and escaped to printable ASCII, and /// doubles use std::format's shortest round-trip form. +/// +/// Each renderer destructures its struct, so a new field breaks the build +/// until it is printed here too. #pragma once @@ -48,7 +51,8 @@ inline std::string rings_text(const std::vector& rings) { for (std::size_t r = 0; r < rings.size(); ++r) { out += r == 0 ? "(" : ", ("; for (std::size_t p = 0; p < rings[r].size(); ++p) { - out += std::format("{}{} {}", p == 0 ? "" : ", ", rings[r][p].lon, rings[r][p].lat); + const auto& [lon, lat] = rings[r][p]; + out += std::format("{}{} {}", p == 0 ? "" : ", ", lon, lat); } out += ')'; } @@ -69,98 +73,109 @@ inline std::string_view layer_name(FireWeatherLayer layer) { } inline std::string to_text(const CategoricalOutlookPayload& payload) { + const auto& [day_offset, features] = payload; std::string out = std::format("CategoricalOutlookPayload day_offset={} features={}\n", - payload.day_offset, payload.features.size()); - for (const OutlookFeature& f : payload.features) { + day_offset, features.size()); + for (const auto& [label, severity, rings, issued_at, valid_from, valid_until] : features) { out += std::format("label={} severity={} issued_at={} valid_from={} valid_until={} rings={}\n", - quote(f.label), int{f.severity}, quote(f.issued_at), quote(f.valid_from), - quote(f.valid_until), rings_text(f.rings)); + quote(label), int{severity}, quote(issued_at), quote(valid_from), + quote(valid_until), rings_text(rings)); } return out; } inline std::string to_text(const ProbOutlookPayload& payload) { - std::string out = - std::format("ProbOutlookPayload day_offset={} hazard={} features={}\n", payload.day_offset, - quote(payload.hazard), payload.features.size()); - for (const ProbOutlookFeature& f : payload.features) { + const auto& [day_offset, payload_hazard, features] = payload; + std::string out = std::format("ProbOutlookPayload day_offset={} hazard={} features={}\n", + day_offset, quote(payload_hazard), features.size()); + for (const auto& [hazard, probability, rings, issued_at, valid_from, valid_until] : features) { out += std::format( "hazard={} probability={} issued_at={} valid_from={} valid_until={} rings={}\n", - quote(f.hazard), f.probability, quote(f.issued_at), quote(f.valid_from), - quote(f.valid_until), rings_text(f.rings)); + quote(hazard), probability, quote(issued_at), quote(valid_from), quote(valid_until), + rings_text(rings)); } return out; } inline std::string to_text(const ConditionalIntensityPayload& payload) { - std::string out = std::format("ConditionalIntensityPayload day={} hazard={} features={}\n", - payload.day, quote(payload.hazard), payload.features.size()); - for (const ConditionalIntensityFeature& f : payload.features) { + const auto& [day, hazard, features] = payload; + std::string out = std::format("ConditionalIntensityPayload day={} hazard={} features={}\n", day, + quote(hazard), features.size()); + for (const auto& [label, cig_level, rings, issued_at, valid_from, valid_until] : features) { out += std::format( "label={} cig_level={} issued_at={} valid_from={} valid_until={} rings={}\n", - quote(f.label), int{f.cig_level}, quote(f.issued_at), quote(f.valid_from), - quote(f.valid_until), rings_text(f.rings)); + quote(label), int{cig_level}, quote(issued_at), quote(valid_from), quote(valid_until), + rings_text(rings)); } return out; } inline std::string to_text(const Day48OutlookPayload& payload) { - std::string out = std::format("Day48OutlookPayload day={} features={}\n", payload.day, - payload.features.size()); - for (const Day48Feature& f : payload.features) { + const auto& [payload_day, features] = payload; + std::string out = + std::format("Day48OutlookPayload day={} features={}\n", payload_day, features.size()); + for (const auto& [day, label, probability, rings, issued_at, valid_from, valid_until] : + features) { out += std::format( "day={} label={} probability={} issued_at={} valid_from={} valid_until={} rings={}\n", - f.day, quote(f.label), f.probability, quote(f.issued_at), quote(f.valid_from), - quote(f.valid_until), rings_text(f.rings)); + day, quote(label), probability, quote(issued_at), quote(valid_from), quote(valid_until), + rings_text(rings)); } return out; } inline std::string to_text(const FireWeatherPayload& payload) { - std::string out = std::format("FireWeatherPayload day={} features={}\n", payload.day, - payload.features.size()); - for (const FireWeatherFeature& f : payload.features) { - out += std::format("day={} layer={} label={} severity={} probability={} issued_at={} " - "valid_from={} valid_until={} rings={}\n", - f.day, layer_name(f.layer), quote(f.label), int{f.severity}, - f.probability, quote(f.issued_at), quote(f.valid_from), - quote(f.valid_until), rings_text(f.rings)); + const auto& [payload_day, features] = payload; + std::string out = + std::format("FireWeatherPayload day={} features={}\n", payload_day, features.size()); + for (const auto& [day, layer, label, severity, probability, rings, issued_at, valid_from, + valid_until] : features) { + out += + std::format("day={} layer={} label={} severity={} probability={} issued_at={} " + "valid_from={} valid_until={} rings={}\n", + day, layer_name(layer), quote(label), int{severity}, probability, + quote(issued_at), quote(valid_from), quote(valid_until), rings_text(rings)); } return out; } inline std::string to_text(const MesoscalePayload& payload) { - std::string out = std::format("MesoscalePayload discussions={}\n", payload.discussions.size()); - for (const MesoscaleDiscussion& d : payload.discussions) { - out += std::format("name={} folder_path={} url={} rings={}\n", quote(d.name), - quote(d.folder_path), quote(d.url), rings_text(d.rings)); + const auto& [discussions] = payload; + std::string out = std::format("MesoscalePayload discussions={}\n", discussions.size()); + for (const auto& [name, folder_path, url, rings] : discussions) { + out += std::format("name={} folder_path={} url={} rings={}\n", quote(name), + quote(folder_path), quote(url), rings_text(rings)); } return out; } inline std::string to_text(const WatchPayload& payload) { - std::string out = std::format("WatchPayload watches={}\n", payload.watches.size()); - for (const Watch& w : payload.watches) { + const auto& [watches] = payload; + std::string out = std::format("WatchPayload watches={}\n", watches.size()); + for (const auto& [number, year, type, sel, is_pds, max_hail_size, max_wind_gust_knots, spc_url, + issued_at, expires_at, rings] : watches) { out += std::format("number={} year={} type={} sel={} is_pds={} max_hail_size={} " "max_wind_gust_knots={} spc_url={} issued_at={} expires_at={} " "rings={}\n", - w.number, w.year, quote(w.type), quote(w.sel), w.is_pds, w.max_hail_size, - w.max_wind_gust_knots, quote(w.spc_url), quote(w.issued_at), - quote(w.expires_at), rings_text(w.rings)); + number, year, quote(type), quote(sel), is_pds, max_hail_size, + max_wind_gust_knots, quote(spc_url), quote(issued_at), quote(expires_at), + rings_text(rings)); } return out; } inline std::string to_text(const StormReportPayload& payload) { - std::string out = std::format("StormReportPayload reports={}\n", payload.reports.size()); - for (const StormReport& r : payload.reports) { - out += - std::format("location=({} {}) type={} type_text={} magnitude={} unit={} city={} " - "county={} state={} wfo={} source={} remark={} valid_at={}\n", - r.location.lon, r.location.lat, quote(r.type), quote(r.type_text), - r.magnitude, quote(r.unit), quote(r.city), quote(r.county), quote(r.state), - quote(r.wfo), quote(r.source), quote(r.remark), quote(r.valid_at)); + const auto& [reports] = payload; + std::string out = std::format("StormReportPayload reports={}\n", reports.size()); + for (const auto& [location, type, type_text, magnitude, unit, city, county, state, wfo, source, + remark, valid_at] : reports) { + const auto& [lon, lat] = location; + out += std::format("location=({} {}) type={} type_text={} magnitude={} unit={} city={} " + "county={} state={} wfo={} source={} remark={} valid_at={}\n", + lon, lat, quote(type), quote(type_text), magnitude, quote(unit), + quote(city), quote(county), quote(state), quote(wfo), quote(source), + quote(remark), quote(valid_at)); } return out; } diff --git a/tests/test_allocation_counter.cpp b/tests/test_allocation_counter.cpp index 2f441ea..575dea1 100644 --- a/tests/test_allocation_counter.cpp +++ b/tests/test_allocation_counter.cpp @@ -4,13 +4,13 @@ #include "support/allocation_counter.hpp" +#include #include #include #include #include #include #include -#include #include namespace { @@ -63,43 +63,66 @@ TEST(AllocationCounter, FreeingOlderMemoryMakesRetainedNegative) { EXPECT_LE(stats.retained_bytes, -1000); } -TEST(AllocationCounter, CountsEveryOperatorNewForm) { - constexpr std::align_val_t kAlignment{64}; +TEST(AllocationCounter, CountsEveryOperatorNewAndDeleteForm) { + constexpr std::size_t kSize = 8; + constexpr std::uintptr_t kAlignmentBytes = 64; + constexpr std::align_val_t kAlignment{kAlignmentBytes}; const AllocationProbe probe; - void* plain = ::operator new(8); - void* array = ::operator new[](8); - void* nothrow = ::operator new(8, std::nothrow); - void* aligned = ::operator new(8, kAlignment); - void* aligned_array = ::operator new[](8, kAlignment, std::nothrow); - for (const void* block : {plain, array, nothrow, aligned, aligned_array}) { - escaped = block; + // Twelve blocks from the eight new forms, one for each delete form. + const std::array single = {::operator new(kSize), ::operator new(kSize, std::nothrow), + ::operator new(kSize)}; + const std::array array = { + ::operator new[](kSize), ::operator new[](kSize, std::nothrow), ::operator new[](kSize)}; + const std::array aligned = {::operator new(kSize, kAlignment), + ::operator new(kSize, kAlignment, std::nothrow), + ::operator new(kSize, kAlignment)}; + const std::array aligned_array = {::operator new[](kSize, kAlignment), + ::operator new[](kSize, kAlignment, std::nothrow), + ::operator new[](kSize, kAlignment)}; + bool aligned_ok = true; + for (const std::array& blocks : {aligned, aligned_array}) { + for (const void* block : blocks) { + aligned_ok = aligned_ok && std::bit_cast(block) % kAlignmentBytes == 0; + } } - const bool aligned_ok = std::bit_cast(aligned) % 64 == 0 && - std::bit_cast(aligned_array) % 64 == 0; - ::operator delete(plain, 8); - ::operator delete[](array); - ::operator delete(nothrow, std::nothrow); - ::operator delete(aligned, 8, kAlignment); - ::operator delete[](aligned_array, kAlignment); + for (const std::array& blocks : {single, array, aligned, aligned_array}) { + for (const void* block : blocks) { + escaped = block; + } + } + ::operator delete(single[0]); + ::operator delete(single[1], kSize); + ::operator delete(single[2], std::nothrow); + ::operator delete[](array[0]); + ::operator delete[](array[1], kSize); + ::operator delete[](array[2], std::nothrow); + ::operator delete(aligned[0], kAlignment); + ::operator delete(aligned[1], kSize, kAlignment); + ::operator delete(aligned[2], kAlignment, std::nothrow); + ::operator delete[](aligned_array[0], kAlignment); + ::operator delete[](aligned_array[1], kSize, kAlignment); + ::operator delete[](aligned_array[2], kAlignment, std::nothrow); const AllocationStats stats = probe.stats(); - EXPECT_EQ(stats.allocations, 5u); - EXPECT_EQ(stats.bytes, 40u); + EXPECT_EQ(stats.allocations, 12u); + EXPECT_EQ(stats.bytes, 12 * kSize); + // Each delete form gave its block back to the count. EXPECT_EQ(stats.retained_bytes, 0); EXPECT_TRUE(aligned_ok); } TEST(AllocationCounter, SeesAllocationsMadeInsideTheStandardLibrary) { - // libc++ and libstdc++ compile std::string's growth into their shared - // library, so this checks that the replacement reaches it there too. + // std::runtime_error copies its message inside libc++ and libstdc++, so + // this checks that the replacement reaches allocations made there. + // std::string growth would not show that: GCC inlines it from C++20 on. + constexpr char kMessage[] = "a message longer than any short-string buffer"; const AllocationProbe probe; - std::string text; - text.append(5000, 'x'); - escaped = text.data(); + const std::runtime_error error{kMessage}; + escaped = error.what(); const AllocationStats stats = probe.stats(); EXPECT_GE(stats.allocations, 1u); - EXPECT_GE(stats.bytes, 5001u); + EXPECT_GE(stats.bytes, sizeof(kMessage)); } TEST(AllocationCounter, StartsEachProbeFromZero) { diff --git a/tests/test_golden.cpp b/tests/test_golden.cpp index 55ddf31..4eca432 100644 --- a/tests/test_golden.cpp +++ b/tests/test_golden.cpp @@ -33,6 +33,7 @@ namespace { using namespace spc; +using test::quote; using test::read_fixture; using test::to_text; @@ -134,7 +135,7 @@ std::string_view excerpt(std::string_view line, std::size_t column) { } /// Where `actual` first departs from `expected`, as a line, a column, and -/// both versions of that line. +/// both versions of that line, escaped so a stray \r or tab shows. std::string first_difference(std::string_view expected, std::string_view actual) { std::size_t line = 1; std::size_t line_start = 0; @@ -149,8 +150,8 @@ std::string first_difference(std::string_view expected, std::string_view actual) const std::string_view want = expected.substr(line_start, expected.find('\n', i) - line_start); const std::string_view got = actual.substr(line_start, actual.find('\n', i) - line_start); return std::format("first difference at line {}, column {}\n expected: {}\n actual: {}", - line, i - line_start + 1, excerpt(want, i - line_start), - excerpt(got, i - line_start)); + line, i - line_start + 1, quote(excerpt(want, i - line_start)), + quote(excerpt(got, i - line_start))); } class Golden : public testing::TestWithParam {};