test: Add mockable steady clock, tests for PCP and NATPMP implementations #31022

pull laanwj wants to merge 5 commits into bitcoin:master from laanwj:2024-10-pcp-tests changing 9 files +767 −15
  1. laanwj commented at 8:52 pm on October 2, 2024: member

    Add a NodeSteadyClock, a steady_clock that can be mocked with millisecond precision. Use this in the PCP implementation.

    Then add a mock for a simple scriptable UDP server,, which is used to test various code paths (including successful mappings, timeouts and errors) in the PCP and NATPMP implementations.

    Includes “net: Add optional length checking to CService::SetSockAddr” from #31014 as a prerequisite.

  2. laanwj added the label Tests on Oct 2, 2024
  3. laanwj added the label Utils/log/libs on Oct 2, 2024
  4. DrahtBot commented at 8:52 pm on October 2, 2024: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/31022.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK darosior
    Concept ACK dergoegge
    Stale ACK sipa

    If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #31014 (net: Use GetAdaptersAddresses to get local addresses on Windows by laanwj)
    • #30988 (Split CConnman by vasild)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

  5. DrahtBot renamed this:
    test: Add mockable steady clock, tests for PCP and NATPMP implementations
    test: Add mockable steady clock, tests for PCP and NATPMP implementations
    on Oct 2, 2024
  6. DrahtBot added the label Needs rebase on Oct 2, 2024
  7. laanwj force-pushed on Oct 3, 2024
  8. laanwj force-pushed on Oct 3, 2024
  9. laanwj force-pushed on Oct 3, 2024
  10. DrahtBot added the label CI failed on Oct 3, 2024
  11. laanwj force-pushed on Oct 3, 2024
  12. bitcoin deleted a comment on Oct 3, 2024
  13. laanwj removed the label Needs rebase on Oct 3, 2024
  14. DrahtBot removed the label CI failed on Oct 3, 2024
  15. bitcoin deleted a comment on Oct 3, 2024
  16. in src/util/time.h:26 in 85a41cb90e outdated
    21@@ -22,6 +22,15 @@ struct NodeClock : public std::chrono::system_clock {
    22 };
    23 using NodeSeconds = std::chrono::time_point<NodeClock, std::chrono::seconds>;
    24 
    25+/** Mockable steady clock in the context of tests, otherwise the system steady clock */
    26+struct NodeSteadyClock : public std::chrono::steady_clock {
    


    dergoegge commented at 9:40 am on October 3, 2024:
    Could we turn SteadyClock into this struct instead? Having both types could be confusing.

    laanwj commented at 10:01 am on October 3, 2024:

    sure, fine with me, agree it’s less confusing (NodeSteadyClock says nothing at all) though i wasn’t sure which of the current usages should be mockable and didn’t want to decide that here

    edit: i’ll rename the structure to something clearer like MockableSteadyClock how about that?


    dergoegge commented at 3:22 pm on October 3, 2024:

    i wasn’t sure which of the current usages should be mockable and didn’t want to decide that here

    fair enough!

    MockableSteadyClock sgtm!


    laanwj commented at 11:29 am on October 4, 2024:

    Should be unambigious now, i’ve renamed it and the methods to set/clear the mock time are included on the clock itself.

     0/**
     1 * Version of SteadyClock that is mockable in the context of tests (set the
     2 * current value with SetMockTime), otherwise the system steady clock.
     3 */
     4struct MockableSteadyClock : public std::chrono::steady_clock {
     5    using time_point = std::chrono::time_point<MockableSteadyClock>;
     6    /** Return current system time or mocked time, if set */
     7    static time_point now() noexcept;
     8    static std::time_t to_time_t(const time_point&) = delete; // unused
     9    static time_point from_time_t(std::time_t) = delete;      // unused
    10
    11    /** Set mock time for testing. */
    12    static void SetMockTime(std::chrono::milliseconds mock_time_in);
    13
    14    /** Clear mock time, go back to system steady clock. */
    15    static void ClearMockTime();
    16};
    
  17. in src/util/time.cpp:68 in 85a41cb90e outdated
    53+};
    54+
    55+void SetMockSteadyTime(std::chrono::milliseconds mock_time_in)
    56+{
    57+    Assert(mock_time_in >= 0s);
    58+    g_mock_steady_time.store(mock_time_in, std::memory_order_relaxed);
    


    dergoegge commented at 9:48 am on October 3, 2024:

    My approach for this would have been to reuse SetMockTime, which would set g_mock_time to the requested mocktime and g_mock_steady_time to max(mock_time_in, g_mock_steady_time) (to ensure it only ever increases).

    The benefit of that approach would be that all existing tests that make use of mock time would now also mock steady time (assuming NodeSteadyClock is used throughout).

    I never implemented and tested this but I was curious if you considered this as well?


    laanwj commented at 10:04 am on October 3, 2024:

    i don’t know.

    i like being able to mock them seperately, and millisecond resolultion instead of second resolution is kind of nice here because it tends to be used for deadlines and timeouts, seconds are kind of clunky there.

    also conceptually i think “max(mock_time_in, g_mock_steady_time)” is a wrong representation of how monotonic time works, it’s basically a CPU cycle counter (from system startup, usually), it isn’t linked to wall time or updates of that at all.

    The reference point generally doesn’t matter, only differences do; in my current tests i in fact reset it in every test case.


    dergoegge commented at 3:25 pm on October 3, 2024:

    in my current tests i in fact reset it in every test case.

    Right, this wouldn’t work with my approach as this is a global.

  18. dergoegge commented at 9:55 am on October 3, 2024: member

    Concept ACK

    Left some comments on the approach for the steady mock time.

  19. laanwj force-pushed on Oct 4, 2024
  20. laanwj force-pushed on Oct 21, 2024
  21. darosior commented at 2:49 pm on November 8, 2024: member
    Concept ACK
  22. in src/test/pcp_tests.cpp:30 in 258b2856cd outdated
    22+        SEND, // Expect send (with optional data)
    23+        RECV, // Expect receive (with data)
    24+        NOP,  // Just delay
    25+    } op;
    26+    std::vector<uint8_t> data;
    27+    int error;
    


    sipa commented at 4:45 pm on December 23, 2024:
    Perhaps document that this represents the expected error.

    laanwj commented at 8:50 pm on January 13, 2025:
    Done. It’s actually the injected error, in case send/receive has to fail.
  23. in src/test/pcp_tests.cpp:37 in 258b2856cd outdated
    29+    TestOp(std::chrono::milliseconds delay_in, Op op_in, const std::vector<uint8_t> &data_in, int error_in):
    30+        delay(delay_in), op(op_in), data(data_in), error(error_in) {}
    31+};
    32+
    33+/// Save the value of CreateSock and restore when the test ends.
    34+class PCPTestingSetup : public BasicTestingSetup
    


    sipa commented at 4:46 pm on December 23, 2024:
    Could be marked final.

    laanwj commented at 8:48 pm on January 13, 2025:

    This doesn’t work, boost apparently does derive from it.

    0.../src/test/pcp_tests.cpp:260:22: error: cannot derive from ‘final’ base ‘PCPTestingSetup’ in derived type ‘pcp_tests::natpmp_ipv4’
    1  260 | BOOST_AUTO_TEST_CASE(natpmp_ipv4)
    2      |                      ^~~~~~~~~~~
    3.../src/test/pcp_tests.cpp:311:22: error: cannot derive from ‘final’ base ‘PCPTestingSetup’ in derived type ‘pcp_tests::pcp_ipv4’
    4  311 | BOOST_AUTO_TEST_CASE(pcp_ipv4)
    5      |                      ^~~~~~~~
    
  24. in src/test/pcp_tests.cpp:78 in 258b2856cd outdated
    73+    const decltype(CreateSock) m_create_sock_orig;
    74+};
    75+
    76+/** Simple scripted UDP server emulation for testing.
    77+ */
    78+class PCPTestSock : public Sock
    


    sipa commented at 4:46 pm on December 23, 2024:
    Could be marked final.

    laanwj commented at 8:50 pm on January 13, 2025:
    Done
  25. in src/test/pcp_tests.cpp:222 in 258b2856cd outdated
    217+
    218+private:
    219+    const std::vector<TestOp> m_script;
    220+    mutable size_t m_script_ptr = 0;
    221+    mutable std::chrono::milliseconds m_time_left;
    222+    mutable std::chrono::milliseconds m_time{1000};
    


    sipa commented at 4:49 pm on December 23, 2024:
    Perhaps document that this must start at nonzero, otherwise MockableSteadyClock::SetMockTime(m_time) would disable mocking.

    laanwj commented at 9:29 pm on December 25, 2024:

    Correct. Thinking of it, we could define a constant for this in the MockableClock like INITIAL_MOCK_TIME. This would document it while at the same time as preventing that mistake being made in future tests that use the mockable steady clock.

    The alternative was to make the mock time in MockableClock an optional, but this would mean it could no longer simply be an atomic value, so was kind of a hassle compared to starting at non-zero.

  26. sipa commented at 4:53 pm on December 23, 2024: member
    Neat, utACK 258b2856cdc6e2c054ea573cf57de7021aebc3c5. I did not verify the test scenario byte sequences against the spec. Just a few nits.
  27. DrahtBot requested review from darosior on Dec 23, 2024
  28. DrahtBot requested review from dergoegge on Dec 23, 2024
  29. in src/netaddress.h:549 in 39f7c14621 outdated
    545+     * @param[in] paddr Pointer to sockaddr structure
    546+     * @param[in] addrlen Length of sockaddr structure in bytes. This will be checked to exactly match the length of
    547+     * a socket address of the provided family, unless std::nullopt is passed
    548+     * @returns true on success
    549+     */
    550+    bool SetSockAddr(const struct sockaddr* paddr, std::optional<socklen_t> addrlen);
    


    darosior commented at 4:42 pm on January 3, 2025:
    This does not need to be optional.

    darosior commented at 7:36 pm on January 13, 2025:
    Ah, maybe it does for #31014.

    laanwj commented at 8:34 pm on January 13, 2025:
    Yes there is one edge-case there where it is necessary for it to be optional, unfortunately, and no length is directlly available. i guess we could also make it non-optional then do something else at the call-site there (e.g. create a AddrLenFromFamily function or such), fine with me too.

    laanwj commented at 8:55 pm on January 13, 2025:
    Ok, made it non-optional now. Somehow feels more consistent. It’s likely that this PR will go in first anyhow. Will look into a different solution for #31014.
  30. DrahtBot added the label Needs rebase on Jan 10, 2025
  31. laanwj force-pushed on Jan 13, 2025
  32. laanwj force-pushed on Jan 13, 2025
  33. laanwj commented at 11:19 am on January 13, 2025: member
    Rebased for merge conflict, and updated for @sipa’s comment (thanks) to add a constant for the initial mocked clock value.
  34. in src/util/time.cpp:54 in e4cc03c189 outdated
    48@@ -48,6 +49,27 @@ std::chrono::seconds GetMockTime()
    49     return g_mock_time.load(std::memory_order_relaxed);
    50 }
    51 
    52+MockableSteadyClock::time_point MockableSteadyClock::now() noexcept
    53+{
    54+    const auto mocktime{g_mock_steady_time.load(std::memory_order_relaxed)};
    


    maflcko commented at 11:19 am on January 13, 2025:

    For the merge conflict, isn’t this missing in the next line?

    0    if (!mocktime.count()) {
    1        g_used_system_time = true;
    2    }
    

    laanwj commented at 11:25 am on January 13, 2025:
    i first didn’t because i thought it would require all tests that indirectly use the steady clock to be updated, which i deemed outside the scope of this PR. But it actually doesn’t, the new clock is (intentially) only used in one place. And besides, this is checked for the fuzz framework not the unit tests. Will add.
  35. laanwj force-pushed on Jan 13, 2025
  36. DrahtBot removed the label Needs rebase on Jan 13, 2025
  37. darosior approved
  38. darosior commented at 7:35 pm on January 13, 2025: member

    ACK a50e1b5af747134b3e102ef7867ee262ba410700

    PROTOCOL_ERROR is the only variant of MappingError which is not covered by the unit test. If you feel that’s worth including it i wrote https://github.com/darosior/bitcoin/commit/be7482c2f6f754cfa4c205cff66ba9c689d426f5 to cover the 3 code paths in which it occurs.

  39. DrahtBot requested review from sipa on Jan 13, 2025
  40. laanwj commented at 8:35 pm on January 13, 2025: member

    PROTOCOL_ERROR is the only variant of MappingError which is not covered by the unit test. If you feel that’s worth including it i wrote https://github.com/darosior/bitcoin/commit/be7482c2f6f754cfa4c205cff66ba9c689d426f5 to cover the 3 code paths in which it occurs.

    Thank you! Will pull that in.

  41. laanwj force-pushed on Jan 13, 2025
  42. net: Add optional length checking to CService::SetSockAddr
    In almost all cases (the only exception is `getifaddrs`), we know the
    size of the data passed into SetSockAddr, so we can check this to be
    what is expected.
    ab1d3ece02
  43. util: Add mockable steady_clock
    This adds a NodeSteadyClock, which is a steady_clock that can be mocked
    with millisecond precision.
    03648321ec
  44. net: Use mockable steady clock in PCP implementation
    This will be needed for the test harness.
    caf9521033
  45. laanwj force-pushed on Jan 13, 2025
  46. darosior commented at 9:22 pm on January 13, 2025: member
    re-ACK 9cd85281c6cc8d3fb01930e53109fae97a108dd8
  47. in src/test/pcp_tests.cpp:161 in 9cd85281c6 outdated
    156+            if (!service.IsBindAny() && service != m_local_ip) {
    157+                return -1;
    158+            }
    159+            m_bound = service;
    160+        }
    161+        return 0;
    


    darosior commented at 5:44 pm on January 15, 2025:

    nit in the context of this test but i think you meant:

    0        if (service.SetSockAddr(sa, sa_len)) {
    1            // Can only bind to one of our local ips
    2            if (!service.IsBindAny() && service != m_local_ip) {
    3                return -1;
    4            }
    5            m_bound = service;
    6            return 0;
    7        }
    8        return -1;
    

    laanwj commented at 1:20 pm on January 16, 2025:
    Yes, failing Bind when the network address fails to set makes more sense.
  48. in src/test/pcp_tests.cpp:100 in 9cd85281c6 outdated
     95+
     96+    ~PCPTestSock() override { m_socket = INVALID_SOCKET; }
     97+
     98+    PCPTestSock& operator=(Sock&& other) override
     99+    {
    100+        assert(false && "Move of Sock into MockSock not allowed.");
    


    darosior commented at 5:45 pm on January 15, 2025:

    nit

    0        assert(false && "Move of Sock into PCPTestSock not allowed.");
    

    laanwj commented at 1:21 pm on January 16, 2025:
    Whoops, so clear where i copied the code from now 😄
  49. in src/test/pcp_tests.cpp:173 in 9cd85281c6 outdated
    168+        return nullptr;
    169+    };
    170+
    171+    int GetSockOpt(int level, int opt_name, void* opt_val, socklen_t* opt_len) const override
    172+    {
    173+        std::memset(opt_val, 0x0, *opt_len);
    


    darosior commented at 5:51 pm on January 15, 2025:
    nit: it’s never used, any reason for defining it?

    laanwj commented at 1:28 pm on January 16, 2025:

    For sake of completeness. i think it’s uglier to fall through to the parent class, which would do an actual getsockopt call on an invalid socket if in the future the called code would use GetSockOpt.

    i kind of wish there was a purely virtual BaseSock and PosixSock and TestSock etc were a subclass of this, so every subclass would be forced to implement everything.

    Could replace with an assert(not implemented) stub. But it’s just one line…


    laanwj commented at 1:56 pm on January 16, 2025:
    Ok, will just make it do nothing and return -1. That’s just as valid, no need fo the memset.
  50. in src/test/pcp_tests.cpp:678 in 9cd85281c6 outdated
    673+            }, 0
    674+        },
    675+        {
    676+            250ms, TestOp::RECV, // 250ms delay before answer
    677+            {
    678+                0x02, 0x81, 0x00, 0x42, // version, opcode, result success
    


    darosior commented at 6:38 pm on January 15, 2025:

    nit

    0                0x02, 0x81, 0x00, 0x42, // version, opcode, result error
    
  51. in src/test/pcp_tests.cpp:96 in 9cd85281c6 outdated
    91+    {
    92+        ElapseTime(std::chrono::seconds(0)); // start mocking steady time
    93+        PrepareOp();
    94+    }
    95+
    96+    ~PCPTestSock() override { m_socket = INVALID_SOCKET; }
    


    darosior commented at 8:09 pm on January 15, 2025:
    Isn’t this redundant with ~Sock?

    laanwj commented at 1:34 pm on January 16, 2025:
    Yes. It makes no sense, it doesn’t even use m_socket. i copied this destructor from StaticContentsSock, which i think also does it redundantly.
  52. darosior commented at 9:30 pm on January 15, 2025: member
    Some more nits i noticed when reusing some of this code for the fuzz target. I don’t think they warrant retouching, but figured i’d post them anyways.
  53. laanwj force-pushed on Jan 16, 2025
  54. laanwj commented at 2:26 pm on January 16, 2025: member
    Updated for @darosior’s suggestions.
  55. in src/test/pcp_tests.cpp:96 in 2b4e54d4b3 outdated
    91+    {
    92+        ElapseTime(std::chrono::seconds(0)); // start mocking steady time
    93+        PrepareOp();
    94+    }
    95+
    96+    ~PCPTestSock() override { }
    


    darosior commented at 2:55 pm on January 16, 2025:

    clang-tidy complains about this:

    [09:51:32.084] [584/666][11.5s] clang-tidy-19 -p=/ci_container_base/ci/scratch/build-x86_64-pc-linux-gnu -quiet -load=/tidy-build/libbitcoin-tidy.so /ci_container_base/src/test/pcp_tests.cpp [09:51:32.084] /ci_container_base/src/test/pcp_tests.cpp:96:5: error: use ‘= default’ to define a trivial destructor [modernize-use-equals-default,-warnings-as-errors] [09:51:32.084] 96 | ~PCPTestSock() override { } [09:51:32.084] | ^ ~~~ [09:51:32.084] | = default; [09:51:32.084] 973 warnings generated.


    laanwj commented at 3:02 pm on January 16, 2025:
    Yes, this is really silly. Linters are useful for preventing constructs that have high risk of bugs but this just seems like busywork. Anyhow, removed the destructor completely and re-pushed.

    maflcko commented at 3:15 pm on January 16, 2025:
    I added this in 3333bae9b2a, because it allows compiler optimizations according to the docs (https://clang.llvm.org/extra/clang-tidy/checks/modernize/use-equals-default.html). Obviously the performance doesn’t matter in most cases (like in the context of this pull). Though, it is also one less thing to bikeshed about. Still, I am happy to have it taken out again.

    laanwj commented at 3:40 pm on January 16, 2025:
    Nah it’s not a big deal i should probably find a way to run the linter stuff locally to prevent having to play ping pong like this.

    maflcko commented at 3:58 pm on January 16, 2025:

    i should probably find a way to run the linter stuff locally

    In theory it became a little bit easier after cmake, but it still requires a configure with clang. The you can possibly use the clang-tidy-diff approach explained in the dev notes. Though, for me it is rare enough to hit, that I am just playing the ping-pong. :sweat_smile:

  56. test: Add tests for PCP and NATPMP implementations
    Add a mock for a simple scriptable UDP server, and use this to test
    various code paths (including successful mappings, timeouts and errors)
    in the PCP and NATPMP implementations.
    fc700bb47f
  57. qa: cover PROTOCOL_ERROR variant in PCP unit tests 0f716f2889
  58. laanwj force-pushed on Jan 16, 2025
  59. DrahtBot added the label CI failed on Jan 16, 2025
  60. DrahtBot commented at 2:59 pm on January 16, 2025: contributor

    🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/35719158891

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  61. maflcko commented at 3:21 pm on January 16, 2025: member
    The msan CI failure seems unrelated, so I’ve rerun it and left a comment here: https://github.com/bitcoin/bitcoin/pull/31397/files#r1918744397
  62. DrahtBot removed the label CI failed on Jan 16, 2025
  63. darosior commented at 7:47 pm on January 16, 2025: member
    re-ACK 0f716f28896c6edfcd4e2a2b25c88f478a029c7b

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2025-01-21 06:12 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me