{
 "number": 35139,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35139",
 "title": "test: Add thread-safe fast-failing test macros",
 "author": "maflcko",
 "author_association": "MEMBER",
 "created_at": "2026-04-22T14:04:15Z",
 "updated_at": "2026-09-05T06:48:10Z",
 "age_days": 148,
 "draft": true,
 "labels": [
  "Tests"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "30702cc5f1fba7b64da5660c451387a01e3b3149",
 "head_ref": "2604-test-fail-fast-thread-safe-macros",
 "head_repo": "maflcko/bitcoin-core",
 "head_history": [
  {
   "t": "2026-04-22T15:04:20Z",
   "sha": "fa2192d4fe270a1cccea7cf975fe194977090647"
  },
  {
   "t": "2026-04-22T19:02:45Z",
   "sha": "fa2769484ec69ede6c6aea7538efdaf054ce9980"
  },
  {
   "t": "2026-04-22T20:03:20Z",
   "sha": "faea07badaf54c399772c65f04613da22b2729fa"
  },
  {
   "t": "2026-04-23T06:11:53Z",
   "sha": "fac40f847ef40af5fc3e8203fbbf5e17af8dcf7f"
  },
  {
   "t": "2026-05-11T06:47:00Z",
   "sha": "86f628181e23b1ba7670ed06f155cb604b77b303"
  },
  {
   "t": "2026-05-11T07:31:15Z",
   "sha": "fa46ae95325f4e17ba2430be0b130cd02c88750f"
  },
  {
   "t": "2026-08-20T12:26:26Z",
   "sha": "30702cc5f1fba7b64da5660c451387a01e3b3149"
  }
 ],
 "additions": 337,
 "deletions": 162,
 "changed_files": 4,
 "commit_count": 4,
 "size_bucket": "L",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "sedited",
      "url": "https://github.com/bitcoin/bitcoin/pull/35139#issuecomment-4904579293"
     }
    ]
   },
   "conflicts": [
    {
     "number": 36091,
     "title": "test: Add debug output to common tested types",
     "author": "rustaceanrob"
    },
    {
     "number": 35713,
     "title": "Remove boost as a unit test runner",
     "author": "rustaceanrob"
    },
    {
     "number": 35003,
     "title": "validation: improve block data I/O error handling in P2P paths",
     "author": "furszy"
    },
    {
     "number": 33324,
     "title": "blocks: add resumable reobfuscation for existing block files",
     "author": "l0rinc"
    }
   ]
  }
 },
 "acks_parsed": {
  "sedited": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-07-07T13:54:27Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 1,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "mercie-ux",
   "sedited"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-08-20T12:26:26Z",
  "last_reviewer_activity": "2026-07-07T13:54:27Z",
  "last_reviewer": "sedited",
  "author_silent_days": 28,
  "waiting_on_author_days": 0,
  "days_since_update": 12
 },
 "refs": {
  "mentioned": [],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [],
  "conflicts": [
   36091,
   35713,
   35003,
   33324
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/test/util/common.h"
 ],
 "body": "Currently, the unit tests use different boost check macros. However, this is problematic:\n\n* The check macros are tied to the boost test framework and can not be used in other tests, such as the fuzz tests.\n* The check macros are not overflow-safe. For example `BOOST_CHECK_GT(-10, 10U);` compiles and passes fine, even though it is obviously wrong.\n* The check macros are not thread-safe. Using them in a thread will compile fine, but then fail tsan later on. Usually it is trivial to replace them by a plain `assert`, but it would be better if the macros just worked under tsan.\n* The check macros are not fast-failing. This is confusing, as the test continues to run and will print more logs, making it harder to spot the first failure. Also, it is contrary to the internal check macros `assert` and `Assert`, and even `CHECK_NONFATAL` under debug mode, which all lead to an immediate abort.\n\nFix all issues by adding new check macros: `ASSERT`, `ASSERT_EQ` (and friends), `ASSERT_EQ_COLLECTIONS` (C++20 range-based), as well as `ASSERT_EXCEPTION`. They can be used as a drop-in replacement for the corresponding boost macro.\n\nThe implementation for all macros is only ~150 LOC, so it should be preferable to any external dependency addressing the above issues (which I couldn't find anyway).\n\n(Tracking issue: https://github.com/bitcoin/bitcoin/issues/34666)\n\n### Note on naming (why `ASSERT`?)\n\nThe test macros follow the naming and semantics of the internal Bitcoin Core macros (`assert`, and `Assert`). While it may confusing to have three unary `assert` macros that do *roughly* the same, it should be harmless and any macro that compiles is fine to use. (There is a special edge case to `Assert`, which allows to be turned into an exception as part of `g_detail_test_only_CheckFailuresAreExceptionsNotAborts`, but this shouldn't matter much).\n\nIn theory, if devs didn't care about nice diagnostic messages on a failure (printing the left and right side), the macros could even be a direct alias of `assert` or `Assert`, reducing the LOC even more. Though, I think it is fine and preferable to have slightly better failure messages in tests.\n\nAlso, the naming and semantics follow the Bitcoin Core functional test macros, which also start with `assert_`.\n\n### Later optimizations\n\nThe macros are currently sitting in the header, but once they are applied to the whole codebase, the pretty-printing implementation can move to a cpp file, which reduces the weight of the header and makes it possible to change or add pretty-printing features without a whole re-compilation of all tests.",
 "commits": [
  {
   "sha": "4685eddb5377ff59e245c01538167543c87b2e72",
   "date": "2026-08-20T12:25:09Z",
   "message": "test: Add test/util/common.cpp for IWYU\n\nThe cpp file will be needed in the future, and also makes IWYU run on the module."
  },
  {
   "sha": "6fcfed98a2fc8fc4affa9a2d223187af1699e080",
   "date": "2026-08-20T12:25:09Z",
   "message": "test: Add thread-safe fast-failing test macros"
  },
  {
   "sha": "3d5a782b8bcacc7ba012c4cc8b50c26eebc8c8ed",
   "date": "2026-08-20T12:26:14Z",
   "message": "scripted-diff: Use new test macros in streams_tests.cpp\n\n-BEGIN VERIFY SCRIPT-\n\n ren() { sed --in-place --regexp-extended \"s|\\<$1\\>|$2|g\" src/test/streams_tests.cpp ; }\n\n # \"Boring\" replacements:\n ren BOOST_CHECK           ASSERT\n ren BOOST_CHECK_EQUAL     ASSERT_EQ\n ren BOOST_REQUIRE_EQUAL   ASSERT_EQ\n ren BOOST_CHECK_EXCEPTION ASSERT_EXCEPTION\n\n # \"Advanced\" replacements:\n # Append empty HasReason operator\n sed --in-place --regexp-extended 's/BOOST_CHECK_THROW\\((.+)\\)/ASSERT_EXCEPTION(\\1, HasReason{\"\"})/g' src/test/streams_tests.cpp\n\n-END VERIFY SCRIPT-"
  },
  {
   "sha": "30702cc5f1fba7b64da5660c451387a01e3b3149",
   "date": "2026-08-20T12:26:14Z",
   "message": "test: Manually replace multi-line BOOST_CHECK_EQUAL_COLLECTIONS macro"
  }
 ],
 "timeline": [
  {
   "t": "2026-04-22T15:04:20Z",
   "kind": "force_push",
   "who": "maflcko",
   "commit": "fa2192d4fe270a1cccea7cf975fe194977090647"
  },
  {
   "t": "2026-04-22T15:42:33Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "To test the Tsan error, one can use this diff:\n\n```diff\ndiff --git a/src/test/btcsignals_tests.cpp b/src/test/btcsignals_tests.cpp\nindex b3203ebeb2..43a68607a3 100644\n--- a/src/test/btcsignals_tests.cpp\n+++ b/src/test/btcsignals_tests.cpp\n@@ -5,2 +5,3 @@\n #include <btcsignals.h>\n+#include <test/util/common.h>\n #include <test/util/setup_common.h>\n@@ -162,3 +163,3 @@ BOOST_AUTO_TEST_CASE(thread_safety)\n         // a race inside of BOOST_CHECK itself (writing to the log).\n-        assert(!sig0.empty());\n+        ASSERT(!sig0.empty());\n         assert(conn0.connected());\n```\n\nTsan passes with `ASSSERT` or `assert`, or `Assert`, but fails with `BOOST_CHECK` when running this test.\n\n<!--\n\n     # Insert the required header\n     sed --in-place '5i#include <test/util/common.h>' src/test/streams_tests.cpp\n\n  # - Use range-based collection equality check\n sed --in-place --regexp-extended 's/BOOST_CHECK_EQUAL_COLLECTIONS\\(([^.]+)\\.begin\\(\\), \\1\\.end\\(\\), ([^.]+)\\.begin\\(\\), \\2\\.end\\(\\)\\)/ASSERT_EQ_COLLECTIONS(\\1, \\2)/g' src/test/streams_tests.cpp"
  },
  {
   "t": "2026-04-22T19:02:45Z",
   "kind": "force_push",
   "who": "maflcko",
   "commit": "fa2769484ec69ede6c6aea7538efdaf054ce9980"
  },
  {
   "t": "2026-04-22T20:03:20Z",
   "kind": "force_push",
   "who": "maflcko",
   "commit": "faea07badaf54c399772c65f04613da22b2729fa"
  },
  {
   "t": "2026-04-22T20:11:44Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "To test the overall behavior, and compare it with the boost behavior, one can use this diff (it includes recommended instructions to run)\n\na diff to trigger different failures\n\n```diff\ndiff --git a/src/test/streams_tests.cpp b/src/test/streams_tests.cpp\nindex 9bf7f6f6fd..e6a4049328 100644\n--- a/src/test/streams_tests.cpp\n+++ b/src/test/streams_tests.cpp\n@@ -17,6 +17,154 @@\n using namespace std::string_literals;\n using namespace util::hex_literals;\n\n+// All suites below will fail\n+// For testing, run them via Bash:\n+// clang-format off\n+// cmake --build ./bld-cmake --parallel && for s in Suite_Bool Suite_Equality Suite_Null Suite_Null2 Suite_Inequality Suite_LessThan Suite_GreaterThan Suite_LessEqual Suite_GreaterEqual Suite_Collections Suite_CollectionsEnd Suite_Exceptions Suite_ExceptionsWrongCatch Suite_ExceptionsWrongThrow; do valgrind ./bld-cmake/bin/test_bitcoin --catch_system_error=no -t $s; echo \"Exit code: $?\"; done\n+// clang-format on\n+BOOST_AUTO_TEST_SUITE(Suite_Bool)\n+BOOST_AUTO_TEST_CASE(Test_Assert)\n+{\n+    short* condition{};\n+    BOOST_CHECK(condition);\n+    ASSERT(condition);\n+}\n+BOOST_AUTO_TEST_SUITE_END()\n+\n+BOOST_AUTO_TEST_SUITE(Suite_Equality)\n+BOOST_AUTO_TEST_CASE(Test_AssertEq)\n+{\n+    const char* a{\"hi\"};\n+    std::cout << \"Passes on both:\\n\";\n+    BOOST_CHECK_EQUAL(a, \"hi\");\n+    ASSERT_EQ(a, \"hi\");\n+\n+    std::cout << \"Fails on the new macro (This check is stricter and will do pointer-compare)\\n\";\n+    char b[3] = {'h', 'i', '\\0'};\n+    BOOST_CHECK_EQUAL(b, \"hi\");\n+    using namespace std::literals;\n+    ASSERT_EQ(b, \"hi\"); // Use \"hi\"sv or \"hi\"s instead\n+}\n+BOOST_AUTO_TEST_SUITE_END()\n+\n+BOOST_AUTO_TEST_SUITE(Suite_Null)\n+BOOST_AUTO_TEST_CASE(Test_AssertNull)\n+{\n+    const char* a{\"hi\"};\n+    BOOST_CHECK_EQUAL(a, nullptr);\n+    ASSERT_EQ(a, nullptr);\n+}\n+BOOST_AUTO_TEST_SUITE_END()\n+\n+BOOST_AUTO_TEST_SUITE(Suite_Null2)\n+BOOST_AUTO_TEST_CASE(Test_AssertNull2)\n+{\n+    const char* a{};\n+    BOOST_CHECK_EQUAL(a, \"hi\");\n+    ASSERT_EQ(a, \"hi\");\n+}\n+BOOST_AUTO_TEST_SUITE_END()\n+\n+BOOST_AUTO_TEST_SUITE(Suite_Inequality)\n+BOOST_AUTO_TEST_CASE(Test_AssertNe)\n+{\n+    bool a{true};\n+    BOOST_CHECK_NE(a, true);\n+    ASSERT_NE(a, true);\n+}\n+BOOST_AUTO_TEST_SUITE_END()\n+\n+BOOST_AUTO_TEST_SUITE(Suite_LessThan)\n+BOOST_AUTO_TEST_CASE(Test_AssertLt)\n+{\n+    BOOST_CHECK_LT(10, 5U);\n+    ASSERT_LT(10U, 5U); // Checks at compile time that the same signdness is provided!\n+}\n+BOOST_AUTO_TEST_SUITE_END()\n+\n+BOOST_AUTO_TEST_SUITE(Suite_GreaterThan)\n+BOOST_AUTO_TEST_CASE(Test_AssertGt)\n+{\n+    std::cout << \"This should obviously fail, but boost passes!\\n\";\n+    BOOST_CHECK_GT(-10, 10U);\n+    // ASSERT_GT(-10, 10U); // does not compile\n+}\n+BOOST_AUTO_TEST_SUITE_END()\n+\n+BOOST_AUTO_TEST_SUITE(Suite_LessEqual)\n+BOOST_AUTO_TEST_CASE(Test_AssertLe)\n+{\n+    BOOST_CHECK_LE(10.1, 5.1);\n+    ASSERT_LE(10.1, 5.1);\n+}\n+BOOST_AUTO_TEST_SUITE_END()\n+\n+BOOST_AUTO_TEST_SUITE(Suite_GreaterEqual)\n+BOOST_AUTO_TEST_CASE(Test_AssertGe)\n+{\n+    BOOST_CHECK_GE(char{12}, 'a');\n+    ASSERT_GE(char{12}, 'a');\n+}\n+BOOST_AUTO_TEST_SUITE_END()\n+\n+BOOST_AUTO_TEST_SUITE(Suite_Collections)\n+BOOST_AUTO_TEST_CASE(Test_AssertCollections)\n+{\n+    std::vector<int> v1{1, 2, 3}, v2{1, 99, 3};\n+    BOOST_CHECK_EQUAL_COLLECTIONS(v1.begin(), v1.end(), v2.begin(), v2.end());\n+    ASSERT_EQ_COLLECTIONS(v1, v2);\n+}\n+BOOST_AUTO_TEST_SUITE_END()\n+\n+BOOST_AUTO_TEST_SUITE(Suite_CollectionsEnd)\n+BOOST_AUTO_TEST_CASE(Test_AssertCollectionsEnd)\n+{\n+    std::vector<NodeSeconds> v1{\n+        NodeSeconds{1s},\n+    },\n+        v2{};\n+    BOOST_CHECK_EQUAL_COLLECTIONS(v1.begin(), v1.end(), v2.begin(), v2.end());\n+    ASSERT_EQ_COLLECTIONS(v1, v2);\n+}\n+BOOST_AUTO_TEST_SUITE_END()\n+\n+BOOST_AUTO_TEST_SUITE(Suite_Exceptions)\n+BOOST_AUTO_TEST_CASE(Test_AssertException)\n+{\n+    auto thrower = []() { throw std::runtime_error(\"test error\"); };\n+    auto predicate = [](const std::runtime_error&) { return false; };\n+\n+    BOOST_CHECK_EXCEPTION(thrower(), std::runtime_error, predicate);\n+    ASSERT_EXCEPTION(thrower(), std::runtime_error, predicate);\n+}\n+BOOST_AUTO_TEST_SUITE_END()\n+\n+BOOST_AUTO_TEST_SUITE(Suite_ExceptionsWrongCatch)\n+BOOST_AUTO_TEST_CASE(Test_AssertExceptionWrongCatch)\n+{\n+    auto thrower = []() { throw std::runtime_error(\"test error\"); };\n+    auto predicate = [](const int&) { return false; };\n+\n+    try {\n+        BOOST_CHECK_EXCEPTION(thrower(), int, predicate);\n+    } catch (...) {\n+        std::cout << \"boost lets the exception through\\n\";\n+    }\n+    ASSERT_EXCEPTION(thrower(), int, predicate);\n+}\n+BOOST_AUTO_TEST_SUITE_END()\n+\n+BOOST_AUTO_TEST_SUITE(Suite_ExceptionsWrongThrow)\n+BOOST_AUTO_TEST_CASE(Test_AssertExceptionWrongThrow)\n+{\n+    auto thrower = []() {};\n+    auto predicate = [](const int&) { return false; };\n+\n+    BOOST_CHECK_EXCEPTION(thrower(), int, predicate);\n+    ASSERT_EXCEPTION(thrower(), int, predicate);\n+}\n+BOOST_AUTO_TEST_SUITE_END()\n+\n BOOST_FIXTURE_TEST_SUITE(streams_tests, BasicTestingSetup)\n\n // Check optimized obfuscation with random offsets and sizes to ensure proper\n\n```"
  },
  {
   "t": "2026-04-23T06:11:53Z",
   "kind": "force_push",
   "who": "maflcko",
   "commit": "fac40f847ef40af5fc3e8203fbbf5e17af8dcf7f"
  },
  {
   "t": "2026-04-27T02:40:03Z",
   "kind": "review_comment",
   "who": "mercie-ux",
   "assoc": "NONE",
   "path": "src/test/util/common.h",
   "commit": "86f628181e23b1ba7670ed06f155cb604b77b303",
   "in_reply_to": null,
   "text": "I have noted  `<typeindex>` appears to be unused here. I checked for any usage of `std::type_index` but didn\u2019t find any.\nSince `typeid(e).name()` relies on `<typeinfo>`, can this include likely be removed to reduce compile run time"
  },
  {
   "t": "2026-05-11T06:47:00Z",
   "kind": "force_push",
   "who": "maflcko",
   "commit": "86f628181e23b1ba7670ed06f155cb604b77b303"
  },
  {
   "t": "2026-05-11T07:31:15Z",
   "kind": "force_push",
   "who": "maflcko",
   "commit": "fa46ae95325f4e17ba2430be0b130cd02c88750f"
  },
  {
   "t": "2026-05-11T07:31:33Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/test/util/common.h",
   "commit": "86f628181e23b1ba7670ed06f155cb604b77b303",
   "in_reply_to": 3144653003,
   "text": "Thx, according to iwyu:\n\n```\n\n---\n\n/home/admin/actions-runner/_work/_temp/src/test/util/common.h should add these lines:\n#include <iterator>         // for distance\n#include <typeinfo>         // for type_info\n#include <utility>          // for declval\n\n/home/admin/actions-runner/_work/_temp/src/test/util/common.h should remove these lines:\n- #include <cstdio>  // lines 13-13\n- #include <functional>  // lines 16-16\n- #include <ostream>  // lines 19-19\n- #include <typeindex>  // lines 26-26\n\nThe full include-list for /home/admin/actions-runner/_work/_temp/src/test/util/common.h:\n#include <tinyformat.h>     // for format, tfm\n#include <algorithm>        // for mismatch\n#include <chrono>           // for time_point\n#include <concepts>         // for same_as\n#include <cstdlib>          // for abort\n#include <exception>        // for exception_ptr, exception, current_exception, rethrow_exception\n#include <iostream>         // for operator<<, ostream, basic_ostream, cerr, endl, ostringstream\n#include <iterator>         // for distance\n#include <optional>         // for optional\n#include <ranges>           // for end, forward_range, begin\n#include <source_location>  // for source_location\n#include <sstream>          // for basic_ostringstream\n#include <string>           // for basic_string, string, char_traits, operator<<\n#include <string_view>      // for string_view, operator<<\n#include <type_traits>      // for is_signed_v, is_integral_v, is_array_v, is_bounded_array_v, is_enum_v, is_pointer_v, remove_all_extents_t, underlying_type_t\n#include <typeinfo>         // for type_info\n#include <utility>          // for declval\n---\n```\n\nPushed the diff."
  },
  {
   "t": "2026-05-26T09:56:48Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "Not sure about these changes. I've found it useful in the past that the tests are not failing fast. Both to have more context when debugging, and to see all failing cases at once when patching them. Judged by the lack of engagement here, I'm also not sure if others would find that valuable too. The value I could see is in the shared utilities between the fuzz and unit tests, but even there we could just use plain asserts, since they are not actual test cases?"
  },
  {
   "t": "2026-05-28T17:13:45Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nI guess this may be subjective. The context before a failure is certainly useful, but the context after a failure is often counter-productive (think about a 10'000 line github log, where the failure happens in the first 1000 lines, or think about a test failure that sets off an avalanche of other failures, where only the first one is the relevant one to fix). Again, this may be subjective (I've heard preferences either way), so I am happy to add an env var to control the behavior here.\n\n[quoted text omitted]\nYeah, I think the main issue is that all the annoyances are not worth it when looked at in a vacuum. However, while the boost test framework was the right choice in 2011, I am not sure it is the right choice in a modern C++ codebase:\n\n* It doesn't work with tsan out of the box when a check macro is called outside the main thread\n* It doesn't work with clang-tidy out of the box when a std::move is done in a check macro (I ran into this, but other seem to be running into it as well: https://bitcoin-irc.chaincode.com/bitcoin-core-dev/2026-05-21#1221768)\n* It silences compiler errors that would normally happen for integer compares of different signs\n* It doesn't work well with iwyu (https://github.com/kevkevinpal/bitcoin/issues/385)\n* Probably more stuff I missed  ...?\n\nI understand the review bottleneck, and I don't want to force this on anyone, but I want to highlight the issues and ask if we want to address them here, not at all, or upstream?"
  },
  {
   "t": "2026-07-07T13:28:13Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nI keep running into this, and it is annoying. E.g. try to find the failure in https://github.com/bitcoin/bitcoin/actions/runs/28867247659/job/85623740486?pr=35676#step:9:2171. The full log doesn't show the failure in the tail of the log:\n\nhttps://gist.github.com/maflcko/4b2b6c7e1a0d23841cbe7d962ed61525"
  },
  {
   "t": "2026-07-07T13:54:27Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "Concept ACK"
  },
  {
   "t": "2026-07-07T14:01:16Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nTurned to draft into now, as a conflicting pull was opened in the meantime.  Probably good to keep the number of non-draft pulls to a minimum."
  },
  {
   "t": "2026-08-20T12:26:26Z",
   "kind": "force_push",
   "who": "maflcko",
   "commit": "30702cc5f1fba7b64da5660c451387a01e3b3149"
  }
 ],
 "labels_log": [
  {
   "t": "2026-04-22T14:04:20Z",
   "action": "labeled",
   "label": "Tests",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-22T15:05:31Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-22T20:50:37Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-11T07:31:49Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-11T08:28:38Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-20T10:23:35Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-20T12:32:57Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-04-22T14:04:19Z",
   "kind": "renamed",
   "who": "DrahtBot",
   "from": " test: Add thread-safe fast-failing test macros ",
   "to": "test: Add thread-safe fast-failing test macros"
  },
  {
   "t": "2026-07-07T13:59:43Z",
   "kind": "convert_to_draft",
   "who": "maflcko"
  }
 ],
 "text_chars": 13914,
 "text_tokens_estimate": 3478,
 "changed_paths": [
  "src/test/streams_tests.cpp",
  "src/test/util/CMakeLists.txt",
  "src/test/util/common.cpp",
  "src/test/util/common.h"
 ],
 "files": [
  {
   "path": "src/test/streams_tests.cpp",
   "add": 155,
   "del": 161
  },
  {
   "path": "src/test/util/CMakeLists.txt",
   "add": 1,
   "del": 0
  },
  {
   "path": "src/test/util/common.cpp",
   "add": 5,
   "del": 0
  },
  {
   "path": "src/test/util/common.h",
   "add": 176,
   "del": 1
  }
 ],
 "test_lines": 499,
 "git": {
  "head": "30702cc5f1fba7b64da5660c451387a01e3b3149",
  "head_matches_backup": true,
  "base": "08dfaa04f4ca4bd58adc9263fa27f1e7017dff62",
  "commits": [
   {
    "sha": "4685eddb53",
    "subject": "test: Add test/util/common.cpp for IWYU",
    "files": 2,
    "add": 6,
    "del": 0
   },
   {
    "sha": "6fcfed98a2",
    "subject": "test: Add thread-safe fast-failing test macros",
    "files": 1,
    "add": 176,
    "del": 1
   },
   {
    "sha": "3d5a782b8b",
    "subject": "scripted-diff: Use new test macros in streams_tests.cpp",
    "files": 1,
    "add": 150,
    "del": 150
   },
   {
    "sha": "30702cc5f1",
    "subject": "test: Manually replace multi-line BOOST_CHECK_EQUAL_COLLECTIONS macro",
    "files": 1,
    "add": 5,
    "del": 11
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "e5725cf0a191d40b",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}