Skip to content

Commit

Permalink
Fix flaky Observer tests using fake clock (UBC-Thunderbots#3318)
Browse files Browse the repository at this point in the history
* Fix observer test flakiness using FakeClock

* Add FakeClock unit tests

* Run formatting

* Use using instead of typedef

* [pre-commit.ci lite] apply automatic fixes

---------

Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
  • Loading branch information
2 people authored and Lmh-java committed Oct 25, 2024
1 parent 208e02d commit 76d855b
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 25 deletions.
1 change: 1 addition & 0 deletions src/software/multithreading/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ cc_test(
deps = [
":observer",
"//shared/test_util:tbots_gtest_main",
"//software/test_util:fake_clock",
],
)

Expand Down
25 changes: 13 additions & 12 deletions src/software/multithreading/observer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
* "Subject<T>" to receive new instances of type T when they are available
*
* @tparam T The type of object this class is observing
* @tparam Clock A clock that satisfies the TrivialClock requirements
*/
template <typename T>
template <typename T, typename Clock = std::chrono::steady_clock>
class Observer
{
public:
Expand Down Expand Up @@ -79,34 +80,34 @@ class Observer
boost::circular_buffer<std::chrono::milliseconds> receive_time_buffer;
};

template <typename T>
Observer<T>::Observer(size_t buffer_size, bool log_buffer_full)
template <typename T, typename Clock>
Observer<T, Clock>::Observer(size_t buffer_size, bool log_buffer_full)
: buffer(buffer_size, log_buffer_full), receive_time_buffer(TIME_BUFFER_SIZE)
{
}

template <typename T>
void Observer<T>::receiveValue(T val)
template <typename T, typename Clock>
void Observer<T, Clock>::receiveValue(T val)
{
receive_time_buffer.push_back(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()));
Clock::now().time_since_epoch()));
buffer.push(std::move(val));
}

template <typename T>
std::optional<T> Observer<T>::popMostRecentlyReceivedValue(Duration max_wait_time)
template <typename T, typename Clock>
std::optional<T> Observer<T, Clock>::popMostRecentlyReceivedValue(Duration max_wait_time)
{
return buffer.popMostRecentlyAddedValue(max_wait_time);
}

template <typename T>
std::optional<T> Observer<T>::popLeastRecentlyReceivedValue(Duration max_wait_time)
template <typename T, typename Clock>
std::optional<T> Observer<T, Clock>::popLeastRecentlyReceivedValue(Duration max_wait_time)
{
return buffer.popLeastRecentlyAddedValue(max_wait_time);
}

template <typename T>
double Observer<T>::getDataReceivedPerSecond()
template <typename T, typename Clock>
double Observer<T, Clock>::getDataReceivedPerSecond()
{
if (receive_time_buffer.empty())
{
Expand Down
26 changes: 13 additions & 13 deletions src/software/multithreading/observer_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

#include <thread>

class TestObserver : public Observer<int>
#include "software/test_util/fake_clock.h"

class TestObserver : public Observer<int, FakeClock>
{
public:
std::optional<int> getMostRecentValueFromBufferWrapper()
Expand All @@ -19,36 +21,34 @@ namespace TestUtil
* Tests getDataReceivedPerSecond by filling the buffer
*
* @param test_observer The observer to test
* @param data_received_period_milliseconds The period between receiving data
* @param data_received_period_ms The period between receiving data in milliseconds
* @param number_of_messages number of messages to send to the buffer
*
* @return AssertionSuccess the observer returns the correct data received per second
*/
::testing::AssertionResult testGetDataReceivedPerSecondByFillingBuffer(
TestObserver test_observer, unsigned int data_received_period_milliseconds,
TestObserver test_observer, unsigned int data_received_period_ms,
unsigned int number_of_messages)
{
auto wall_time_start = std::chrono::steady_clock::now();
auto wall_time_start = FakeClock::now();
for (unsigned int i = 0; i < number_of_messages; i++)
{
test_observer.receiveValue(i);
std::this_thread::sleep_for(
std::chrono::milliseconds(data_received_period_milliseconds));
FakeClock::advance(std::chrono::milliseconds(data_received_period_ms));
}

auto wall_time_now = std::chrono::steady_clock::now();
auto wall_time_now = FakeClock::now();
double test_duration_s =
static_cast<double>(std::chrono::duration_cast<std::chrono::milliseconds>(
wall_time_now - wall_time_start)
.count()) *
SECONDS_PER_MILLISECOND;
double scaling_factor =
test_duration_s / (data_received_period_milliseconds *
SECONDS_PER_MILLISECOND * number_of_messages);
double expected_actual_difference =
std::abs(test_observer.getDataReceivedPerSecond() -
1 / (data_received_period_milliseconds * SECONDS_PER_MILLISECOND) *
scaling_factor);
test_duration_s /
(data_received_period_ms * SECONDS_PER_MILLISECOND * number_of_messages);
double expected_actual_difference = std::abs(
test_observer.getDataReceivedPerSecond() -
1 / (data_received_period_ms * SECONDS_PER_MILLISECOND) * scaling_factor);
if (expected_actual_difference < 50)
{
return ::testing::AssertionSuccess();
Expand Down
15 changes: 15 additions & 0 deletions src/software/test_util/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ cc_library(
],
)

cc_library(
name = "fake_clock",
srcs = ["fake_clock.cpp"],
hdrs = ["fake_clock.h"],
)

cc_test(
name = "test_util_test",
srcs = ["test_util_test.cpp"],
Expand All @@ -50,3 +56,12 @@ cc_test(
"//shared/test_util:tbots_gtest_main",
],
)

cc_test(
name = "fake_clock_test",
srcs = ["fake_clock_test.cpp"],
deps = [
":fake_clock",
"//shared/test_util:tbots_gtest_main",
],
)
18 changes: 18 additions & 0 deletions src/software/test_util/fake_clock.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#include "fake_clock.h"

FakeClock::time_point FakeClock::now_;

FakeClock::time_point FakeClock::now() noexcept
{
return now_;
}

void FakeClock::advance(duration time) noexcept
{
now_ += time;
}

void FakeClock::reset() noexcept
{
now_ = FakeClock::time_point();
}
52 changes: 52 additions & 0 deletions src/software/test_util/fake_clock.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#pragma once

#include <chrono>
#include <cstdint>
#include <ratio>

/**
* Fake clock convenient for testing purposes.
*
* Tests that rely on sleeping can be non-deterministic and flaky since they
* depend on real time to elapse. Use this clock instead, which can be explicitly
* advanced without sleeping.
*
* Satisfies the TrivialClock requirements and thus can replace any standard clock
* in the chrono library (e.g. std::chrono::steady_clock).
*/
class FakeClock
{
public:
using rep = uint64_t;
using period = std::nano;
using duration = std::chrono::duration<rep, period>;
using time_point = std::chrono::time_point<FakeClock>;
inline static const bool is_steady = false;

/**
* Returns the current time point.
*
* @return the current time point
*/
static time_point now() noexcept;

/**
* Advances the current time point by the given amount of time.
*
* @param time the amount of time to advance the current time point by
*/
static void advance(duration time) noexcept;

/**
* Resets the current time point to the clock's epoch
* (the origin of fake_clock::time_point)
*/
static void reset() noexcept;

private:
FakeClock() = delete;
~FakeClock() = delete;
FakeClock(FakeClock const&) = delete;

static time_point now_;
};
39 changes: 39 additions & 0 deletions src/software/test_util/fake_clock_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#include "software/test_util/fake_clock.h"

#include <gtest/gtest.h>

TEST(FakeClockTest, time_point_now_should_not_change_without_advancing)
{
FakeClock::reset();
FakeClock::time_point t0 = FakeClock::now();
FakeClock::time_point t1 = FakeClock::now();
EXPECT_EQ(std::chrono::milliseconds(0), t1 - t0);
}

TEST(FakeClockTest, time_point_now_should_move_forward_after_advancing)
{
FakeClock::reset();
FakeClock::time_point t0 = FakeClock::now();

FakeClock::advance(std::chrono::milliseconds(100));
FakeClock::time_point t1 = FakeClock::now();
EXPECT_EQ(std::chrono::milliseconds(100), t1 - t0);

FakeClock::advance(std::chrono::milliseconds(50));
FakeClock::time_point t2 = FakeClock::now();
EXPECT_EQ(std::chrono::milliseconds(150), t2 - t0);
}

TEST(FakeClockTest, reset_should_set_time_point_now_to_clock_epoch)
{
FakeClock::reset();
FakeClock::time_point t0 = FakeClock::now();

FakeClock::advance(std::chrono::milliseconds(1000));
FakeClock::time_point t1 = FakeClock::now();
EXPECT_EQ(std::chrono::milliseconds(1000), t1 - t0);

FakeClock::reset();
FakeClock::time_point t2 = FakeClock::now();
EXPECT_EQ(std::chrono::milliseconds(0), t2 - t0);
}

0 comments on commit 76d855b

Please sign in to comment.