diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 6d17245b3..af028dc64 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -24,7 +24,8 @@ jobs: strategy: matrix: - container: ['gcc:5', 'gcc:7', 'gcc:9', 'gcc:10', 'gcc:11', 'gcc:12', 'gcc:13'] + container: + ["gcc:5", "gcc:7", "gcc:9", "gcc:10", "gcc:11", "gcc:12", "gcc:13"] # The type of runner that the job will run on runs-on: ubuntu-latest @@ -33,9 +34,7 @@ jobs: # Use the container for this specific version of gcc container: ${{ matrix.container }} - # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - name: Checkout Code uses: actions/checkout@v3 @@ -57,7 +56,7 @@ jobs: run: cmake --build build --config Release --parallel 2 - name: Test - timeout-minutes: 5 + timeout-minutes: 10 # Execute tests defined by the CMake configuration. # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail run: | @@ -66,13 +65,9 @@ jobs: build-osx: name: MacOS Clang - - # The type of runner that the job will run on runs-on: macos-latest - # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - name: Checkout Code uses: actions/checkout@v3 @@ -87,7 +82,7 @@ jobs: run: cmake --build build --config Release --parallel 2 - name: Test - timeout-minutes: 5 + timeout-minutes: 10 # Execute tests defined by the CMake configuration. # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail run: | @@ -100,9 +95,7 @@ jobs: # The type of runner that the job will run on runs-on: windows-latest - # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - name: Checkout Code uses: actions/checkout@v3 @@ -117,7 +110,7 @@ jobs: run: cmake --build build --config Release --parallel 2 - name: Test - timeout-minutes: 5 + timeout-minutes: 10 # Execute tests defined by the CMake configuration. # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail run: | @@ -127,21 +120,17 @@ jobs: check-clang-tidy-linux: name: Clang-Tidy Linux - - # The type of runner that the job will run on runs-on: ubuntu-latest - # Steps represent a sequence of tasks that will be executed as part of the job steps: - name: Install clang-tidy-15 run: | - wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - - echo "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-15 main" | sudo tee /etc/apt/sources.list.d/llvm-15 - echo "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-15 main" | sudo tee -a /etc/apt/sources.list.d/llvm-15 - sudo apt-get update - sudo apt-get install -y clang-tidy-15 + wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - + echo "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-15 main" | sudo tee /etc/apt/sources.list.d/llvm-15 + echo "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-15 main" | sudo tee -a /etc/apt/sources.list.d/llvm-15 + sudo apt-get update + sudo apt-get install -y clang-tidy-15 - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - name: Checkout Code uses: actions/checkout@v3 @@ -164,13 +153,9 @@ jobs: check-clang-tidy-msvc: name: Clang-Tidy MSVC - - # The type of runner that the job will run on runs-on: windows-latest - # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - name: Checkout Code uses: actions/checkout@v3 diff --git a/CMakeLists.txt b/CMakeLists.txt index e500f8ab9..4bfd2163e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,8 +13,14 @@ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +cmake_minimum_required(VERSION 3.15.0) -cmake_minimum_required(VERSION 3.1.0) +# Set the project after the build type as the Project command can change the build type +project( + NUClear + VERSION 1.0.0 + LANGUAGES C CXX +) # We use additional modules that cmake needs to know about set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/") @@ -35,13 +41,6 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") endif() -# Set the project after the build type as the Project command can change the build type -project( - NUClear - VERSION 1.0.0 - LANGUAGES C CXX -) - # NUClear targets c++14 set(CMAKE_CXX_STANDARD 14) @@ -51,6 +50,12 @@ if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) set(MASTER_PROJECT ON) endif() +if(MSVC) + add_compile_options(/W4) +else() + add_compile_options(-Wall -Wextra -pedantic) +endif(MSVC) + # If this option is set we are building using continous integration option(CI_BUILD "Enable build options for building in the CI server" OFF) diff --git a/src/Environment.hpp b/src/Environment.hpp index 69afe8dba..9381604f6 100644 --- a/src/Environment.hpp +++ b/src/Environment.hpp @@ -40,8 +40,8 @@ class PowerPlant; */ class Environment { public: - Environment(PowerPlant& powerplant, std::string&& reactor_name, LogLevel log_level) - : powerplant(powerplant), log_level(log_level), reactor_name(reactor_name) {} + Environment(PowerPlant& powerplant, std::string reactor_name, const LogLevel& log_level) + : powerplant(powerplant), reactor_name(std::move(reactor_name)), log_level(log_level) {} private: friend class PowerPlant; @@ -49,10 +49,10 @@ class Environment { /// @brief The PowerPlant to use in this reactor PowerPlant& powerplant; - /// @brief The log level for this reactor - LogLevel log_level; /// @brief The name of the reactor std::string reactor_name; + /// @brief The log level for this reactor + LogLevel log_level; }; } // namespace NUClear diff --git a/src/PowerPlant.cpp b/src/PowerPlant.cpp index 2bc0509c0..c39ccc0b7 100644 --- a/src/PowerPlant.cpp +++ b/src/PowerPlant.cpp @@ -37,8 +37,9 @@ void PowerPlant::start() { // We are now running is_running.store(true); - // Direct emit startup event + // Direct emit startup event and command line arguments emit(std::make_unique()); + emit_shared(dsl::store::DataStore::get()); // Start all of the threads scheduler.start(); diff --git a/src/PowerPlant.ipp b/src/PowerPlant.ipp index 349584b3b..635122010 100644 --- a/src/PowerPlant.ipp +++ b/src/PowerPlant.ipp @@ -45,9 +45,8 @@ inline PowerPlant::PowerPlant(Configuration config, int argc, const char* argv[] args.emplace_back(argv[i]); } - // We emit this twice, so the data is available for extensions + // Emit our command line arguments emit(std::make_unique(args)); - emit(std::make_unique(args)); } template diff --git a/src/Reactor.hpp b/src/Reactor.hpp index 2bc02f542..fa1d92979 100644 --- a/src/Reactor.hpp +++ b/src/Reactor.hpp @@ -159,7 +159,7 @@ class Reactor { std::vector reaction_handles{}; public: - /// @brief TODO + /// @brief The powerplant that this reactor is running in PowerPlant& powerplant; /// @brief The demangled string name of this reactor diff --git a/src/dsl/word/IO.hpp b/src/dsl/word/IO.hpp index 3b5f83ce9..bd5079d79 100644 --- a/src/dsl/word/IO.hpp +++ b/src/dsl/word/IO.hpp @@ -31,12 +31,35 @@ namespace NUClear { namespace dsl { namespace word { +#ifdef _WIN32 + using event_t = long; // NOLINT(google-runtime-int) +#else + using event_t = short; // NOLINT(google-runtime-int) +#endif + + /** + * @brief This message is sent to the IO controller to configure a new IO operation. + */ struct IOConfiguration { + IOConfiguration(fd_t fd, event_t events, std::shared_ptr reaction) + : fd(fd), events(events), reaction(std::move(reaction)) {} + /// @brief The file descriptor to watch fd_t fd; - int events; + /// @brief The events to watch for on this file descriptor + event_t events; + /// @brief The reaction to trigger when this file descriptor has an event std::shared_ptr reaction; }; + /** + * @brief This is emitted when an IO operation has finished. + */ + struct IOFinished { + IOFinished(const uint64_t& id) : id(id) {} + /// @brief The id of the reaction that has finished + uint64_t id; + }; + /** * @brief * This is used to trigger reactions based on standard I/O operations using file descriptors. @@ -68,30 +91,43 @@ namespace dsl { * @par Implements * Bind */ - struct IO : public Single { + struct IO { // On windows we use different wait events #ifdef _WIN32 // NOLINTNEXTLINE(google-runtime-int) - enum EventType : short{READ = FD_READ | FD_OOB | FD_ACCEPT, WRITE = FD_WRITE, CLOSE = FD_CLOSE, ERROR = 0}; + enum EventType : event_t { + READ = FD_READ | FD_OOB | FD_ACCEPT, + WRITE = FD_WRITE, + CLOSE = FD_CLOSE, + ERROR = 0, + }; #else // NOLINTNEXTLINE(google-runtime-int) - enum EventType : short { READ = POLLIN, WRITE = POLLOUT, CLOSE = POLLHUP, ERROR = POLLNVAL | POLLERR }; + enum EventType : event_t { + READ = POLLIN, + WRITE = POLLOUT, + CLOSE = POLLHUP, + ERROR = POLLNVAL | POLLERR, + }; #endif struct Event { + /// @brief The file descriptor that this event is for fd_t fd; - int events; + /// @brief The events that have occurred on this file descriptor + event_t events; + /// @brief Returns true if the event is for the given event type operator bool() const { - return fd != -1; + return fd != INVALID_SOCKET; } }; using ThreadEventStore = dsl::store::ThreadStore; template - static inline void bind(const std::shared_ptr& reaction, fd_t fd, int watch_set) { + static inline void bind(const std::shared_ptr& reaction, fd_t fd, event_t watch_set) { reaction->unbinders.push_back([](const threading::Reaction& r) { r.reactor.emit(std::make_unique>(r.id)); @@ -114,6 +150,11 @@ namespace dsl { // Otherwise return an invalid event return Event{INVALID_SOCKET, 0}; } + + template + static inline void postcondition(threading::ReactionTask& task) { + task.parent.reactor.emit(std::make_unique(task.parent.id)); + } }; } // namespace word diff --git a/src/dsl/word/UDP.hpp b/src/dsl/word/UDP.hpp index 3deb2f9e5..5fd0e7134 100644 --- a/src/dsl/word/UDP.hpp +++ b/src/dsl/word/UDP.hpp @@ -333,7 +333,6 @@ namespace dsl { template static inline RecvResult read(threading::Reaction& reaction) { - // Get our file descriptor from the magic cache auto event = IO::get(reaction); @@ -429,7 +428,7 @@ namespace dsl { p.local = Packet::Target{local_s.first, local_s.second}; p.remote = Packet::Target{remote_s.first, remote_s.second}; - // Confirm that this packet was sent to one of our broadcast addresses + // Confirm that this packet was sent to one of our local addresses for (const auto& iface : util::network::get_interfaces()) { if (iface.ip.sock.sa_family == result.local.sock.sa_family) { // If the two are equal diff --git a/src/dsl/word/Watchdog.hpp b/src/dsl/word/Watchdog.hpp index 97a977000..beffe5518 100644 --- a/src/dsl/word/Watchdog.hpp +++ b/src/dsl/word/Watchdog.hpp @@ -266,11 +266,11 @@ namespace dsl { // Check if our watchdog has timed out if (NUClear::clock::now() > (service_time + period(ticks))) { - // Submit the reaction to the thread pool + // Submit the reaction to the thread pool reaction->reactor.powerplant.submit(reaction->get_task()); // Now automatically service the watchdog - time = NUClear::clock::now() + period(ticks); + time += period(ticks); } // Change our wait time to our new watchdog time else { diff --git a/src/extension/IOController_Posix.hpp b/src/extension/IOController_Posix.hpp index a7039b500..aeebaa8b0 100644 --- a/src/extension/IOController_Posix.hpp +++ b/src/extension/IOController_Posix.hpp @@ -35,99 +35,281 @@ namespace extension { class IOController : public Reactor { private: + /// @brief The type that poll uses for events + using event_t = decltype(pollfd::events); + + /** + * @brief A task that is waiting for an IO event + */ struct Task { Task() = default; // NOLINTNEXTLINE(google-runtime-int) - Task(const fd_t& fd, short events, std::shared_ptr reaction) - : fd(fd), events(events), reaction(std::move(reaction)) {} + Task(const fd_t& fd, event_t listening_events, std::shared_ptr reaction) + : fd(fd), listening_events(listening_events), reaction(std::move(reaction)) {} + /// @brief The file descriptor we are waiting on fd_t fd{-1}; - short events{0}; // NOLINT(google-runtime-int) + /// @brief The events that the task is interested in + event_t listening_events{0}; + /// @brief The events that are waiting to be fired + event_t waiting_events{0}; + /// @brief The events that are currently being processed + event_t processing_events{0}; + /// @brief The reaction that is waiting for this event std::shared_ptr reaction{nullptr}; + /** + * @brief Sorts the tasks by their file descriptor + * + * The tasks are sorted by file descriptor so that when we rebuild the list of file descriptors to poll we + * can assume that if the same file descriptor shows up multiple times it will be next to each other. This + * allows the events that are being watched to be or'ed together. + * + * @param other the other task to compare to + * + * @return true if this task is less than the other + * @return false if this task is greater than or equal to the other + */ bool operator<(const Task& other) const { - return fd == other.fd ? events < other.events : fd < other.fd; + return fd == other.fd ? listening_events < other.listening_events : fd < other.fd; } }; + /** + * @brief Rebuilds the list of file descriptors to poll + * + * This function is called when the list of file descriptors to poll changes. It will rebuild the list of file + * descriptors used by poll + */ + void rebuild_list() { + // Get the lock so we don't concurrently modify the list + const std::lock_guard lock(tasks_mutex); + + // Clear our fds to be rebuilt + watches.resize(0); + + // Insert our notify fd + watches.push_back(pollfd{notify_recv, POLLIN, 0}); + + for (const auto& r : tasks) { + // If we are the same fd, then add our interest set + if (r.fd == watches.back().fd) { + watches.back().events = event_t(watches.back().events | r.listening_events); + } + // Otherwise add a new one + else { + watches.push_back(pollfd{r.fd, r.listening_events, 0}); + } + } + + // We just cleaned the list! + dirty = false; + } + + /** + * @brief Fires the event for the task if it is ready + * + * @param task the task to try to fire the event for + * + * @return the iterator to the next task in the list + */ + void fire_event(Task& task) { + if (task.processing_events == 0 && task.waiting_events != 0) { + + // Make our event to pass through and store it in the local cache + IO::Event e{}; + e.fd = task.fd; + e.events = task.waiting_events; + + // Clear the waiting events, we are now processing them + task.processing_events = task.waiting_events; + task.waiting_events = 0; + + // Submit the task (which should run the get) + IO::ThreadEventStore::value = &e; + std::unique_ptr r = task.reaction->get_task(); + IO::ThreadEventStore::value = nullptr; + + if (r != nullptr) { + powerplant.submit(std::move(r)); + } + else { + task.waiting_events = event_t(task.waiting_events | task.processing_events); + task.processing_events = 0; + } + } + } + + /** + * @brief Collects the events that have happened and sets them up to fire + */ + void process_events() { + + // Get the lock so we don't concurrently modify the list + const std::lock_guard lock(tasks_mutex); + + for (auto& fd : watches) { + + // Something happened + if (fd.revents != 0) { + + // It's our notification handle + if (fd.fd == notify_recv) { + // Read our value to clear it's read status + char val = 0; + if (::read(fd.fd, &val, sizeof(char)) < 0) { + throw std::system_error(network_errno, + std::system_category(), + "There was an error reading our notification pipe?"); + }; + } + // It's a regular handle + else { + // Check if we have a read event but 0 bytes to read, this can happen when a socket is closed + // On linux we don't get a close event, we just keep getting read events with 0 bytes + // To make the close event happen if we get a read event with 0 bytes we will check if there are + // any currently processing reads and if not, then close + bool maybe_eof = false; + if ((fd.revents & IO::READ) != 0) { + int bytes_available = 0; + const bool valid = ::ioctl(fd.fd, FIONREAD, &bytes_available) == 0; + if (valid && bytes_available == 0) { + maybe_eof = true; + } + } + + // Find our relevant tasks + auto range = std::equal_range(tasks.begin(), + tasks.end(), + Task{fd.fd, 0, nullptr}, + [](const Task& a, const Task& b) { return a.fd < b.fd; }); + + // There are no tasks for this! + if (range.first == tasks.end()) { + // If this happens then our list is definitely dirty... + dirty = true; + } + else { + // Loop through our values + for (auto it = range.first; it != range.second; ++it) { + // Load in the relevant events that happened into the waiting events + it->waiting_events = event_t(it->waiting_events | (it->listening_events & fd.revents)); + + if (maybe_eof && (it->processing_events & IO::READ) == 0) { + it->waiting_events |= IO::CLOSE; + } + + fire_event(*it); + } + } + } + + // Clear the events from poll to avoid double firing + fd.revents = 0; + } + } + } + + /** + * @brief Bumps the notification pipe to wake up the poll command + * + * If the poll command is waiting it will wait forever if something doesn't happen. + * When trying to update what to poll or shut down we need to wake it up so it can. + */ + // NOLINTNEXTLINE(readability-make-member-function-const) this changes states + void bump() { + // Check if there was an error + uint8_t val = 1; + if (::write(notify_send, &val, sizeof(val)) < 0) { + throw std::system_error(network_errno, + std::system_category(), + "There was an error while writing to the notification pipe"); + } + } + public: - explicit IOController(std::unique_ptr environment) - : Reactor(std::move(environment)), notify_recv(), notify_send() { + explicit IOController(std::unique_ptr environment) : Reactor(std::move(environment)) { std::array vals = {-1, -1}; - - const int i = ::pipe(vals.data()); + const int i = ::pipe(vals.data()); if (i < 0) { throw std::system_error(network_errno, std::system_category(), "We were unable to make the notification pipe for IO"); } - notify_recv = vals[0]; notify_send = vals[1]; - // Add our notification pipe to our list of fds - fds.push_back(pollfd{notify_recv, POLLIN, 0}); + // Start by rebuliding the list + rebuild_list(); on>().then( "Configure IO Reaction", [this](const dsl::word::IOConfiguration& config) { // Lock our mutex to avoid concurrent modification - const std::lock_guard lock(reaction_mutex); + const std::lock_guard lock(tasks_mutex); // NOLINTNEXTLINE(google-runtime-int) - reactions.emplace_back(config.fd, static_cast(config.events), config.reaction); + tasks.emplace_back(config.fd, event_t(config.events), config.reaction); // Resort our list - std::sort(std::begin(reactions), std::end(reactions)); + std::sort(tasks.begin(), tasks.end()); // Let the poll command know that stuff happened dirty = true; + bump(); + }); - // Check if there was an error - if (::write(notify_send, &dirty, 1) < 0) { - throw std::system_error(network_errno, - std::system_category(), - "There was an error while writing to the notification pipe"); - } + on>().then("IO Finished", [this](const dsl::word::IOFinished& event) { + // Get the lock so we don't concurrently modify the list + const std::lock_guard lock(tasks_mutex); + + // Find the reaction that finished processing + auto task = std::find_if(tasks.begin(), tasks.end(), [&event](const Task& t) { + return t.reaction->id == event.id; }); + // If we found it then clear the waiting events + if (task != tasks.end()) { + // If the events we were processing included close remove it from the list + if ((task->processing_events & IO::CLOSE) != 0) { + dirty = true; + tasks.erase(task); + } + else { + // We have finished processing events + task->processing_events = 0; + + // Try to fire again which will check if there are any waiting events + fire_event(*task); + } + } + }); + on>>().then( "Unbind IO Reaction", [this](const dsl::operation::Unbind& unbind) { // Lock our mutex to avoid concurrent modification - const std::lock_guard lock(reaction_mutex); + const std::lock_guard lock(tasks_mutex); // Find our reaction - auto reaction = std::find_if(std::begin(reactions), std::end(reactions), [&unbind](const Task& t) { + auto reaction = std::find_if(tasks.begin(), tasks.end(), [&unbind](const Task& t) { return t.reaction->id == unbind.id; }); - if (reaction != std::end(reactions)) { - reactions.erase(reaction); + if (reaction != tasks.end()) { + tasks.erase(reaction); } // Let the poll command know that stuff happened dirty = true; - if (::write(notify_send, &dirty, 1) < 0) { - throw std::system_error(network_errno, - std::system_category(), - "There was an error while writing to the notification pipe"); - } + bump(); }); on().then("Shutdown IO Controller", [this] { // Set shutdown to true so it won't try to poll again shutdown.store(true); - // A byte to send down the pipe - char val = 0; - - // Send a single byte down the pipe - if (::write(notify_send, &val, 1) < 0) { - throw std::system_error(network_errno, - std::system_category(), - "There was an error while writing to the notification pipe"); - } + bump(); }); on().then("IO Controller", [this] { @@ -135,130 +317,40 @@ namespace extension { // shutdown keeps us out here if (!shutdown.load()) { + // Rebuild the list if something changed + if (dirty) { + rebuild_list(); + } - // TODO(trent): check for dirty here - - - // Poll our file descriptors for events - const int result = ::poll(fds.data(), static_cast(fds.size()), -1); - - // Check if we had an error on our Poll request - if (result < 0) { + // Wait for an event to happen on one of our file descriptors + if (::poll(watches.data(), nfds_t(watches.size()), -1) < 0) { throw std::system_error(network_errno, std::system_category(), "There was an IO error while attempting to poll the file descriptors"); } - for (auto& fd : fds) { - - // Something happened - if (fd.revents != 0) { - - // It's our notification handle - if (fd.fd == notify_recv) { - // Read our value to clear it's read status - char val = 0; - if (::read(fd.fd, &val, sizeof(char)) < 0) { - throw std::system_error(network_errno, - std::system_category(), - "There was an error reading our notification pipe?"); - }; - } - // It's a regular handle - else { - - // Find our relevant reactions - auto range = std::equal_range(std::begin(reactions), - std::end(reactions), - Task{fd.fd, 0, nullptr}, - [](const Task& a, const Task& b) { return a.fd < b.fd; }); - - - // There are no reactions for this! - if (range.first == std::end(reactions)) { - // If this happens then our list is definitely dirty... - dirty = true; - } - else { - - - // TODO(trent): we also want to swap this element to the back of the list and - // remove it so that it does not fire again - - - // Loop through our values - for (auto it = range.first; it != range.second; ++it) { - - // We should emit if the reaction is interested - if ((it->events & fd.revents) != 0) { - - // Make our event to pass through - IO::Event e{}; - e.fd = fd.fd; - - // Evaluate and store our set in thread store - e.events = fd.revents; - - // Store the event in our thread local cache - IO::ThreadEventStore::value = &e; - - // Submit the task - powerplant.submit(it->reaction->get_task()); - - // Reset our value - IO::ThreadEventStore::value = nullptr; - - // TODO(trent): If we had a close, or error stop listening? - } - } - } - } - - // Reset our events - fd.revents = 0; - } - } - - // If our list is dirty - if (dirty) { - // Get the lock so we don't concurrently modify the list - const std::lock_guard lock(reaction_mutex); - - // Clear our fds to be rebuilt - fds.resize(0); - - // Insert our notify fd - fds.push_back(pollfd{notify_recv, POLLIN, 0}); - - for (const auto& r : reactions) { - - // If we are the same fd, then add our interest set - if (r.fd == fds.back().fd) { - // NOLINTNEXTLINE(google-runtime-int) - fds.back().events = static_cast(fds.back().events | r.events); - } - // Otherwise add a new one - else { - fds.push_back(pollfd{r.fd, r.events, 0}); - } - } - - // We just cleaned the list! - dirty = false; - } + // Collect the events that happened into the tasks list + process_events(); } }); } private: + /// @brief The receive file descriptor for our notification pipe fd_t notify_recv{-1}; + /// @brief The send file descriptor for our notification pipe fd_t notify_send{-1}; + /// @brief Whether or not we are shutting down std::atomic shutdown{false}; + /// @brief The mutex that protects the tasks list + std::mutex tasks_mutex; + /// @brief Whether or not the list of file descriptors is dirty compared to tasks bool dirty = true; - std::mutex reaction_mutex; - std::vector fds{}; - std::vector reactions{}; + /// @brief The list of file descriptors to poll + std::vector watches{}; + /// @brief The list of tasks that are waiting for IO events + std::vector tasks{}; }; } // namespace extension diff --git a/src/extension/IOController_Windows.hpp b/src/extension/IOController_Windows.hpp index 79d1fc386..19393c2c6 100644 --- a/src/extension/IOController_Windows.hpp +++ b/src/extension/IOController_Windows.hpp @@ -28,22 +28,169 @@ namespace NUClear { namespace extension { class IOController : public Reactor { - public: - explicit IOController(std::unique_ptr environment) : Reactor(std::move(environment)) { + private: + /// @brief The type that poll uses for events + using event_t = long; // NOLINT(google-runtime-int) + + /** + * @brief A task that is waiting for an IO event + */ + struct Task { + Task() = default; + Task(const fd_t& fd, event_t listening_events, std::shared_ptr reaction) + : fd(fd), listening_events(listening_events), reaction(std::move(reaction)) {} + + /// @brief The socket we are waiting on + fd_t fd; + /// @brief The events that the task is interested in + event_t listening_events{0}; + /// @brief The events that are waiting to be fired + event_t waiting_events{0}; + /// @brief The events that are currently being processed + event_t processing_events{0}; + /// @brief The reaction that is waiting for this event + std::shared_ptr reaction{nullptr}; + }; + + /** + * @brief Rebuilds the list of file descriptors to poll + * + * This function is called when the list of file descriptors to poll changes. It will rebuild the list of file + * descriptors used by poll + */ + void rebuild_list() { + // Get the lock so we don't concurrently modify the list + const std::lock_guard lock(tasks_mutex); + + // Clear our fds to be rebuilt + watches.resize(0); + + // Insert our notify fd + watches.push_back(notifier); + + for (const auto& r : tasks) { + watches.push_back(r.first); + } + + // We just cleaned the list! + dirty = false; + } + + /** + * @brief Fires the event for the task if it is ready + * + * @param task the task to try to fire the event for + * + * @return the iterator to the next task in the list + */ + void fire_event(Task& task) { + if (task.processing_events == 0 && task.waiting_events != 0) { + + // Make our event to pass through and store it in the local cache + IO::Event e{}; + e.fd = task.fd; + e.events = task.waiting_events; + + // Clear the waiting events, we are now processing them + task.processing_events = task.waiting_events; + task.waiting_events = 0; + + // Submit the task (which should run the get) + IO::ThreadEventStore::value = &e; + std::unique_ptr r = task.reaction->get_task(); + IO::ThreadEventStore::value = nullptr; + + if (r != nullptr) { + powerplant.submit(std::move(r)); + } + else { + // Waiting events are still waiting + task.waiting_events |= task.processing_events; + task.processing_events = 0; + } + } + } + + /** + * @brief Collects the events that have happened and sets them up to fire + */ + void process_event(const WSAEVENT& event) { + + // Get the lock so we don't concurrently modify the list + const std::lock_guard lock(tasks_mutex); + + if (event == notifier) { + // Reset the notifier signal + if (!WSAResetEvent(event)) { + throw std::system_error(WSAGetLastError(), + std::system_category(), + "WSAResetEvent() for notifier failed"); + } + } + else { + // Get our associated Event object, which has the reaction + auto r = tasks.find(event); + + // If it was found... + if (r != tasks.end()) { + // Enum the socket events to work out which ones fired + WSANETWORKEVENTS wsae; + if (WSAEnumNetworkEvents(r->second.fd, event, &wsae) == SOCKET_ERROR) { + throw std::system_error(WSAGetLastError(), + std::system_category(), + "WSAEnumNetworkEvents() failed"); + } + + r->second.waiting_events |= wsae.lNetworkEvents; + fire_event(r->second); + } + // If we can't find the event then our list is dirty + else { + dirty = true; + } + } + } - // Startup WSA for IO - WORD version = MAKEWORD(2, 2); - WSADATA wsa_data; + /** + * @brief Bumps the notification pipe to wake up the poll command + * + * If the poll command is waiting it will wait forever if something doesn't happen. + * When trying to update what to poll or shut down we need to wake it up so it can. + */ + // NOLINTNEXTLINE(readability-make-member-function-const) this changes states + void bump() { + if (!WSASetEvent(notifier)) { + throw std::system_error(WSAGetLastError(), + std::system_category(), + "WSASetEvent() for configure io reaction failed"); + } + } - int startup_status = WSAStartup(version, &wsa_data); - if (startup_status != 0) { - throw std::system_error(startup_status, std::system_category(), "WSAStartup() failed"); + /** + * @brief Removes a task from the list and closes the event + * + * @param it the iterator to the task to remove + * + * @return the iterator to the next task + */ + std::map::iterator remove_task(std::map::iterator it) { + // Close the event + WSAEVENT event = it->first; + + // Remove the task + auto new_it = tasks.erase(it); + + // Try to close the WSA event + if (!WSACloseEvent(event)) { + throw std::system_error(WSAGetLastError(), std::system_category(), "WSACloseEvent() failed"); } - // Reserve 1024 event slots - // Hopefully we won't have more events than that - // Even if we do it should be fine (after a glitch) - events.reserve(1024); + return new_it; + } + + + public: + explicit IOController(std::unique_ptr environment) : Reactor(std::move(environment)) { // Create an event to use for the notifier (used for getting out of WSAWaitForMultipleEvents()) notifier = WSACreateEvent(); @@ -53,14 +200,14 @@ namespace extension { "WSACreateEvent() for notifier failed"); } - // We always have the notifier in the event list - events.push_back(notifier); + // Start by rebuliding the list + rebuild_list(); on>().then( "Configure IO Reaction", [this](const dsl::word::IOConfiguration& config) { // Lock our mutex - std::lock_guard lock(reaction_mutex); + std::lock_guard lock(tasks_mutex); // Make an event for this SOCKET auto event = WSACreateEvent(); @@ -76,177 +223,109 @@ namespace extension { } // Add all the information to the list and mark the list as dirty, to sync with the list of events - reactions.insert(std::make_pair(event, Event{config.fd, config.reaction, config.events})); - reactions_list_dirty = true; + tasks.insert(std::make_pair(event, Task{config.fd, config.events, config.reaction})); + dirty = true; - // Signal the notifier event to return from WSAWaitForMultipleEvents() and sync the dirty list - if (!WSASetEvent(notifier)) { - throw std::system_error(WSAGetLastError(), - std::system_category(), - "WSASetEvent() for configure io reaction failed"); - } + bump(); }); - on>>().then( - "Unbind IO Reaction", - [this](const dsl::operation::Unbind& unbind) { - // Lock our mutex - std::lock_guard lock(reaction_mutex); - - // Find this reaction in our list of reactions - auto reaction = std::find_if(std::begin(reactions), - std::end(reactions), - [&unbind](const std::pair& item) { - return item.second.reaction->id == unbind.id; - }); - - // If the reaction was found - if (reaction != std::end(reactions)) { - // Remove it from the list of reactions - reactions.erase(reaction); - - // Queue the associated event for closing when we sync - events_to_close.push_back(reaction->first); + on>().then("IO Finished", [this](const dsl::word::IOFinished& event) { + // Get the lock so we don't concurrently modify the list + const std::lock_guard lock(tasks_mutex); + + // Find the reaction that finished processing + auto it = std::find_if(tasks.begin(), tasks.end(), [&event](const std::pair& t) { + return t.second.reaction->id == event.id; + }); + + // If we found it then clear the waiting events + if (it != tasks.end()) { + auto& task = it->second; + // If the events we were processing included close remove it from the list + if (task.processing_events & IO::CLOSE) { + dirty = true; + remove_task(it); } else { - // Fail silently: we've unbound a reaction that somehow isn't in our list of reactions! + // We have finished processing events + task.processing_events = 0; + + // Try to fire again which will check if there are any waiting events + fire_event(task); } + } + }); + + on>>().then( + "Unbind IO Reaction", + [this](const dsl::operation::Unbind& unbind) { + // Lock our mutex to avoid concurrent modification + const std::lock_guard lock(tasks_mutex); - // Flag that our list is dirty - reactions_list_dirty = true; + // Find our reaction + auto it = std::find_if(tasks.begin(), tasks.end(), [&unbind](const std::pair& t) { + return t.second.reaction->id == unbind.id; + }); - // Signal the notifier event to return from WSAWaitForMultipleEvents() and sync the dirty list - if (!WSASetEvent(notifier)) { - throw std::system_error(WSAGetLastError(), - std::system_category(), - "WSASetEvent() for unbind io reaction failed"); + if (it != tasks.end()) { + remove_task(it); } + + // Let the poll command know that stuff happened + dirty = true; + bump(); }); on().then("Shutdown IO Controller", [this] { - // Set shutdown to true + // Set shutdown to true so it won't try to poll again shutdown.store(true); - - // Signal the notifier event to return from WSAWaitForMultipleEvents() and shutdown - if (!WSASetEvent(notifier)) { - throw std::system_error(WSAGetLastError(), - std::system_category(), - "WSASetEvent() for shutdown failed"); - } + bump(); }); on().then("IO Controller", [this] { + // To make sure we don't get caught in a weird loop + // shutdown keeps us out here if (!shutdown.load()) { + + // Rebuild the list if something changed + if (dirty) { + rebuild_list(); + } + // Wait for events - auto event_index = WSAWaitForMultipleEvents(static_cast(events.size()), - events.data(), + auto event_index = WSAWaitForMultipleEvents(static_cast(watches.size()), + watches.data(), false, WSA_INFINITE, false); // Check if the return value is an event in our list - if (event_index >= WSA_WAIT_EVENT_0 && event_index < WSA_WAIT_EVENT_0 + events.size()) { + if (event_index >= WSA_WAIT_EVENT_0 && event_index < WSA_WAIT_EVENT_0 + watches.size()) { // Get the signalled event - auto& event = events[event_index - WSA_WAIT_EVENT_0]; - - if (event == notifier) { - // Reset the notifier signal - if (!WSAResetEvent(event)) { - throw std::system_error(WSAGetLastError(), - std::system_category(), - "WSAResetEvent() for notifier failed"); - } - } - else { - // Get our associated Event object, which has the reaction - auto r = reactions.find(event); - - // If it was found... - if (r != reactions.end()) { - // Enum the socket events to work out which ones fired - WSANETWORKEVENTS wsae; - if (WSAEnumNetworkEvents(r->second.fd, event, &wsae) == SOCKET_ERROR) { - throw std::system_error(WSAGetLastError(), - std::system_category(), - "WSAEnumNetworkEvents() failed"); - } - - // Make our IO event to pass through - IO::Event io_event; - io_event.fd = r->second.fd; - - // The events that fired are what we got from the enum events call - io_event.events = wsae.lNetworkEvents; - - // Store the IO event in our thread local cache - IO::ThreadEventStore::value = &io_event; - - // Submit the task - powerplant.submit(r->second.reaction->get_task()); - - // Reset our value - IO::ThreadEventStore::value = nullptr; - } - } - } - } + auto& event = watches[event_index - WSA_WAIT_EVENT_0]; - if (reactions_list_dirty || !events_to_close.empty()) { - // Get the lock so we don't concurrently modify the list - std::lock_guard lock(reaction_mutex); - - // Close any events we've queued for closing - if (!events_to_close.empty()) { - for (auto& event : events_to_close) { - if (!WSACloseEvent(event)) { - throw std::system_error(WSAGetLastError(), - std::system_category(), - "WSACloseEvent() failed"); - } - } - - // Clear the queue of closed events - events_to_close.clear(); + // Collect the events that happened into the tasks list + process_event(event); } - - // Clear the list of events, to be rebuilt - events.resize(0); - - // Add back the notifier event - events.push_back(notifier); - - // Sync the list of reactions to the list of events - for (const auto& r : reactions) { - events.push_back(r.first); - } - - // The list has been synced - reactions_list_dirty = false; } }); } - // We need a destructor to cleanup WSA stuff - virtual ~IOController() { - WSACleanup(); - } - private: - struct Event { - SOCKET fd; - std::shared_ptr reaction; - int events; - }; - + /// @brief The event that is used to wake up the WaitForMultipleEvents call WSAEVENT notifier; + /// @brief Whether or not we are shutting down std::atomic shutdown{false}; - bool reactions_list_dirty = false; - - std::mutex reaction_mutex; - std::map reactions; - std::vector events; - std::vector events_to_close; + /// @brief The mutex that protects the tasks list + std::mutex tasks_mutex; + /// @brief Whether or not the list of file descriptors is dirty compared to tasks + bool dirty = true; + + /// @brief The list of tasks that are currently being processed + std::vector watches; + /// @brief The list of tasks that are waiting for IO events + std::map tasks; }; } // namespace extension diff --git a/src/extension/network/NUClearNetwork.hpp b/src/extension/network/NUClearNetwork.hpp index 754e9c743..e68f3d742 100644 --- a/src/extension/network/NUClearNetwork.hpp +++ b/src/extension/network/NUClearNetwork.hpp @@ -178,7 +178,7 @@ namespace extension { const std::string& address, in_port_t port, const std::string& bind_address = "", - uint16_t network_mtu = 1500); + uint16_t network_mtu = 1500); void reset(const std::string& name, const std::string& address, in_port_t port, diff --git a/src/util/network/get_interfaces.cpp b/src/util/network/get_interfaces.cpp index 9849a0f0e..97ac46d8e 100644 --- a/src/util/network/get_interfaces.cpp +++ b/src/util/network/get_interfaces.cpp @@ -62,8 +62,7 @@ namespace util { for (auto uaddr = addr->FirstUnicastAddress; uaddr != nullptr; uaddr = uaddr->Next) { - Interface iface; - std::memset(&iface, 0, sizeof(iface)); + Interface iface{}; iface.name = addr->AdapterName; @@ -146,10 +145,6 @@ namespace util { std::vector ifaces; - addrinfo hints{}; - std::memset(&hints, 0, sizeof(hints)); - hints.ai_family = AF_INET; - // Query our interfaces ifaddrs* addrs{}; if (::getifaddrs(&addrs) < 0) { @@ -159,53 +154,39 @@ namespace util { } // Loop through our interfaces - for (ifaddrs* cursor = addrs; cursor != nullptr; cursor = cursor->ifa_next) { + for (ifaddrs* it = addrs; it != nullptr; it = it->ifa_next) { // Sometimes we find an interface with no IP (like a CAN bus) this is not what we're after - if (cursor->ifa_addr != nullptr) { + if (it->ifa_addr != nullptr) { - Interface iface; - iface.name = cursor->ifa_name; + Interface iface{}; + iface.name = it->ifa_name; // Copy across our various addresses - switch (cursor->ifa_addr->sa_family) { - case AF_INET: std::memcpy(&iface.ip, cursor->ifa_addr, sizeof(sockaddr_in)); break; - - case AF_INET6: std::memcpy(&iface.ip, cursor->ifa_addr, sizeof(sockaddr_in6)); break; + switch (it->ifa_addr->sa_family) { + case AF_INET: std::memcpy(&iface.ip, it->ifa_addr, sizeof(sockaddr_in)); break; + case AF_INET6: std::memcpy(&iface.ip, it->ifa_addr, sizeof(sockaddr_in6)); break; + default: continue; } - if (cursor->ifa_netmask != nullptr) { - switch (cursor->ifa_addr->sa_family) { - case AF_INET: std::memcpy(&iface.netmask, cursor->ifa_netmask, sizeof(sockaddr_in)); break; - - case AF_INET6: - std::memcpy(&iface.netmask, cursor->ifa_netmask, sizeof(sockaddr_in6)); - break; + if (it->ifa_netmask != nullptr) { + switch (it->ifa_addr->sa_family) { + case AF_INET: std::memcpy(&iface.netmask, it->ifa_netmask, sizeof(sockaddr_in)); break; + case AF_INET6: std::memcpy(&iface.netmask, it->ifa_netmask, sizeof(sockaddr_in6)); break; } } - else { - std::memset(&iface.netmask, 0, sizeof(iface.netmask)); - } - - if (cursor->ifa_dstaddr != nullptr) { - switch (cursor->ifa_addr->sa_family) { - case AF_INET: - std::memcpy(&iface.broadcast, cursor->ifa_dstaddr, sizeof(sockaddr_in)); - break; - case AF_INET6: - std::memcpy(&iface.broadcast, cursor->ifa_dstaddr, sizeof(sockaddr_in6)); - break; + if (it->ifa_dstaddr != nullptr) { + switch (it->ifa_addr->sa_family) { + case AF_INET: std::memcpy(&iface.broadcast, it->ifa_dstaddr, sizeof(sockaddr_in)); break; + case AF_INET6: std::memcpy(&iface.broadcast, it->ifa_dstaddr, sizeof(sockaddr_in6)); break; } } - else { - std::memset(&iface.broadcast, 0, sizeof(iface.broadcast)); - } - iface.flags.broadcast = (cursor->ifa_flags & IFF_BROADCAST) != 0; - iface.flags.loopback = (cursor->ifa_flags & IFF_LOOPBACK) != 0; - iface.flags.pointtopoint = (cursor->ifa_flags & IFF_POINTOPOINT) != 0; - iface.flags.multicast = (cursor->ifa_flags & IFF_MULTICAST) != 0; + iface.flags.broadcast = (it->ifa_flags & IFF_BROADCAST) != 0; + iface.flags.loopback = (it->ifa_flags & IFF_LOOPBACK) != 0; + iface.flags.pointtopoint = (it->ifa_flags & IFF_POINTOPOINT) != 0; + iface.flags.multicast = (it->ifa_flags & IFF_MULTICAST) != 0; ifaces.push_back(iface); } diff --git a/src/util/network/if_number_from_address.cpp b/src/util/network/if_number_from_address.cpp index 540271e4f..8c7934d40 100644 --- a/src/util/network/if_number_from_address.cpp +++ b/src/util/network/if_number_from_address.cpp @@ -39,8 +39,8 @@ namespace util { // Find the correct interface to join on (the one that has our bind address) for (auto& iface : get_interfaces()) { - // iface must be, ipv6, multicast, and have the same address as our bind address - if (iface.ip.sock.sa_family == AF_INET6 && iface.flags.multicast + // iface must be, ipv6, and have the same address as our bind address + if (iface.ip.sock.sa_family == AF_INET6 && ::memcmp(iface.ip.ipv6.sin6_addr.s6_addr, ipv6.sin6_addr.s6_addr, sizeof(in6_addr)) == 0) { // Get the interface for this @@ -49,7 +49,10 @@ namespace util { } // If we get here then we couldn't find an interface - throw std::runtime_error("Could not find interface for address"); + sock_t s{}; + s.ipv6 = ipv6; + auto a = s.address(); + throw std::runtime_error("Could not find interface for address " + a.first + " (is it up?)"); } } // namespace network diff --git a/src/util/platform.cpp b/src/util/platform.cpp index 4f97dba42..e05b69c16 100644 --- a/src/util/platform.cpp +++ b/src/util/platform.cpp @@ -21,6 +21,7 @@ #ifdef _WIN32 #include + #include LPFN_WSARECVMSG WSARecvMsg = nullptr; // Go get that WSARecvMsg function from stupid land @@ -70,6 +71,23 @@ int sendmsg(fd_t fd, msghdr* msg, int flags) { return v == 0 ? sent : v; } + +WSAHolder::WSAHolder() { + WORD version = MAKEWORD(2, 2); + WSADATA wsa_data; + + int startup_status = WSAStartup(version, &wsa_data); + if (startup_status != 0) { + throw std::system_error(startup_status, std::system_category(), "WSAStartup() failed"); + } +} + +WSAHolder::~WSAHolder() { + WSACleanup(); +} + +WSAHolder WSAHolder::instance{}; + } // namespace NUClear #endif diff --git a/src/util/platform.hpp b/src/util/platform.hpp index 191af7bfb..cb177cc82 100644 --- a/src/util/platform.hpp +++ b/src/util/platform.hpp @@ -153,6 +153,19 @@ using socklen_t = int; int recvmsg(fd_t fd, msghdr* msg, int flags); int sendmsg(fd_t fd, msghdr* msg, int flags); + +/** + * @brief This struct is used to setup WSA on windows in a single place so we don't have to worry about it + * + * A single instance of this struct will be created statically at startup which will ensure that WSA is setup for the + * lifetime of the program and torn down as it exists. + */ +struct WSAHolder { + static WSAHolder instance; + WSAHolder(); + ~WSAHolder(); +}; + } // namespace NUClear #else diff --git a/src/util/precise_sleep.cpp b/src/util/precise_sleep.cpp index 266e8997f..1fd2957fa 100644 --- a/src/util/precise_sleep.cpp +++ b/src/util/precise_sleep.cpp @@ -15,6 +15,7 @@ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #include "precise_sleep.hpp" #if defined(_WIN32) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index df9aa1e97..a08231576 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -23,13 +23,9 @@ if(CATCH_FOUND) if(BUILD_TESTS) enable_testing() - if(MSVC) - add_compile_options(/W4 /WX) - else() - add_compile_options(-Wall -Wextra -pedantic -Werror) - endif(MSVC) + add_compile_definitions(CATCH_CONFIG_CONSOLE_WIDTH=120) - file(GLOB test_src test.cpp "api/*.cpp" "dsl/*.cpp" "dsl/emit/*.cpp" "log/*.cpp" "util/*.cpp") + file(GLOB test_src test.cpp "api/*.cpp" "dsl/*.cpp" "dsl/emit/*.cpp" "log/*.cpp" "util/*.cpp" "util/network/*.cpp" "test_util/*.cpp") # Some tests must be executed as individual binaries file(GLOB individual_tests "${CMAKE_CURRENT_SOURCE_DIR}/individual/*.cpp") @@ -40,6 +36,7 @@ if(CATCH_FOUND) add_executable(${test_name} ${test_src}) target_link_libraries(${test_name} NUClear::nuclear) set_target_properties(${test_name} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/individual") + target_include_directories(${test_name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories( ${test_name} SYSTEM PRIVATE ${CATCH_INCLUDE_DIRS} ${PROJECT_BINARY_DIR}/include "${PROJECT_SOURCE_DIR}/src" @@ -51,6 +48,7 @@ if(CATCH_FOUND) endforeach(test_src) add_executable(test_nuclear ${test_src}) + target_include_directories(test_nuclear PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(test_nuclear NUClear::nuclear) target_include_directories( test_nuclear SYSTEM @@ -60,6 +58,7 @@ if(CATCH_FOUND) add_executable(test_network networktest.cpp) target_link_libraries(test_network NUClear::nuclear) + target_include_directories(test_network PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories( test_network SYSTEM PRIVATE ${CATCH_INCLUDE_DIRS} ${PROJECT_BINARY_DIR}/include "${PROJECT_SOURCE_DIR}/src" diff --git a/tests/api/ReactionHandle.cpp b/tests/api/ReactionHandle.cpp index bfb08a611..eeb412c46 100644 --- a/tests/api/ReactionHandle.cpp +++ b/tests/api/ReactionHandle.cpp @@ -19,37 +19,67 @@ #include #include +#include "test_util/TestBase.hpp" + // Anonymous namespace to keep everything file local namespace { -template -struct Message {}; +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +struct Message { + Message(int i) : i(i) {} + int i; +}; -class TestReactor : public NUClear::Reactor { +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { // Make an always disabled reaction - ReactionHandle a = - on>, Priority::HIGH>().then([] { FAIL("This reaction is disabled always"); }); + a = on, Priority::HIGH>().then([](const Message& msg) { // + events.push_back("Executed disabled reaction " + std::to_string(msg.i)); + }); a.disable(); - const ReactionHandle b = on>>().then([this] { powerplant.shutdown(); }); + // Make a reaction that we toggle on and off + b = on, Priority::HIGH>().then([this](const Message& msg) { // + events.push_back("Executed toggled reaction " + std::to_string(msg.i)); + b.disable(); + emit(std::make_unique(1)); + }); + + on>().then([](const Message& msg) { // + events.push_back("Executed enabled reaction " + std::to_string(msg.i)); + }); // Start our test - on().then([this] { emit(std::make_unique>()); }); + on().then([this] { // + emit(std::make_unique(0)); + }); } + + ReactionHandle a{}; + ReactionHandle b{}; }; } // namespace TEST_CASE("Testing reaction handle functionality", "[api][reactionhandle]") { - NUClear::PowerPlant::Configuration config; config.thread_count = 1; NUClear::PowerPlant plant(config); - - // We are installing with an initial log level of debug plant.install(); - plant.start(); + + const std::vector expected = { + "Executed toggled reaction 0", + "Executed enabled reaction 0", + "Executed enabled reaction 1", + }; + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/api/ReactionStatistics.cpp b/tests/api/ReactionStatistics.cpp index fb7e14e64..bddef652c 100644 --- a/tests/api/ReactionStatistics.cpp +++ b/tests/api/ReactionStatistics.cpp @@ -19,85 +19,94 @@ #include #include -// Anonymous namespace to keep everything file local -namespace { +#include "test_util/TestBase.hpp" -template -struct Message {}; +// This namespace is named to make things consistent with the reaction statistics test +namespace stats_test { -struct LoopMsg {}; +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -bool seen_message0 = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -bool seen_message_startup = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +template +struct Message {}; +struct LoopMessage {}; using NUClear::message::ReactionStatistics; -class TestReactor : public NUClear::Reactor { +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { - on>().then("Reaction Stats Handler 2", [this](const ReactionStatistics&) { - // This reaction is just here to cause a potential looping of reaction statistics handlers - emit(std::make_unique()); + // This reaction is here to emit something from a ReactionStatistics trigger + // This shouldn't cause reaction statistics of their own otherwise everything would explode + on>().then("Loop Statistics", [this](const ReactionStatistics&) { + emit(std::make_unique()); }); + on>().then("No Statistics", [] {}); - on>().then("NoStats", [] { - // This guy is triggered by someone triggering on reaction statistics, don't run - }); on>().then("Reaction Stats Handler", [this](const ReactionStatistics& stats) { - // If we are seeing ourself, fail - REQUIRE(stats.identifiers.name != "Reaction Stats Handler"); - - // If we are seeing the other reaction statistics handler, fail - REQUIRE(stats.identifiers.name != "Reaction Stats Handler 2"); - - // If we are seeing the other reaction statistics handler, fail - REQUIRE(stats.identifiers.name != "NoStats"); - - // Flag if we have seen the message handler - if (stats.identifiers.name == "Message Handler") { - seen_message0 = true; - } - // Flag if we have seen the startup handler - else if (stats.identifiers.name == "Startup Handler") { - seen_message_startup = true; + // Other reactions statistics run on this because of built in NUClear reactors (e.g. chrono controller etc) + // We want to filter those out so only our own stats are shown + if (stats.identifiers.name.empty() || stats.identifiers.reactor != reactor_name) { + return; } + events.push_back("Stats for " + stats.identifiers.name + " from " + stats.identifiers.reactor); + events.push_back(stats.identifiers.dsl); // Ensure exceptions are passed through correctly in the exception handler if (stats.exception) { - REQUIRE(stats.identifiers.name == "Exception Handler"); try { std::rethrow_exception(stats.exception); } catch (const std::exception& e) { - REQUIRE(seen_message0); - REQUIRE(seen_message_startup); - REQUIRE(std::string(e.what()) == std::string("Exceptions happened")); - - // We are done - powerplant.shutdown(); + events.push_back("Exception received: \"" + std::string(e.what()) + "\""); } } }); - on>>().then("Message Handler", [this] { emit(std::make_unique>()); }); + on>>().then("Exception Handler", [] { + events.push_back("Running Exception Handler"); + throw std::runtime_error("Text in an exception"); + }); - on>>().then("Exception Handler", [] { throw std::runtime_error("Exceptions happened"); }); + on>>().then("Message Handler", [this] { + events.push_back("Running Message Handler"); + emit(std::make_unique>()); + }); - on().then("Startup Handler", [this] { emit(std::make_unique>()); }); + on().then("Startup Handler", [this] { + events.push_back("Running Startup Handler"); + emit(std::make_unique>()); + }); } }; -} // namespace +} // namespace stats_test TEST_CASE("Testing reaction statistics functionality", "[api][reactionstatistics]") { NUClear::PowerPlant::Configuration config; config.thread_count = 1; NUClear::PowerPlant plant(config); - - // We are installing with an initial log level of debug - plant.install(); - + plant.install(); plant.start(); + + const std::vector expected = { + "Running Startup Handler", + "Stats for Startup Handler from stats_test::TestReactor", + "NUClear::Reactor::on", + "Running Message Handler", + "Stats for Message Handler from stats_test::TestReactor", + "NUClear::Reactor::on>>", + "Running Exception Handler", + "Stats for Exception Handler from stats_test::TestReactor", + "NUClear::Reactor::on>>", + "Exception received: \"Text in an exception\"", + }; + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, stats_test::events)); + + // Check the events fired in order and only those events + REQUIRE(stats_test::events == expected); } diff --git a/tests/dsl/Always.cpp b/tests/dsl/Always.cpp index 293195ae1..f7853c6ea 100644 --- a/tests/dsl/Always.cpp +++ b/tests/dsl/Always.cpp @@ -19,52 +19,67 @@ #include #include +#include "test_util/TestBase.hpp" + namespace { -struct BlankMessage {}; -int i = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -bool emitted_message = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +struct SimpleMessage {}; -class TestReactor : public NUClear::Reactor { +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment), false) { on().then([this] { - // Run until it's 11 then emit the blank message - if (i > 10) { - if (!emitted_message) { - emitted_message = true; - emit(std::make_unique()); - } - } - else { + if (i < 10) { + events.push_back("Always " + std::to_string(i)); ++i; } + else if (i == 10) { + emit(std::make_unique()); + } }); - on>().then([this] { - if (i == 11) { - ++i; - powerplant.shutdown(); - } + on>().then([this] { + events.push_back("Always with SimpleMessage " + std::to_string(i)); + + // We need to shutdown manually as the default pool will always be idle + powerplant.shutdown(); }); } + + /// Counter for the number of times we have run + int i = 0; }; } // namespace -TEST_CASE("Testing on functionality (permanent run)", "[api][always]") { +TEST_CASE("The Always DSL keyword runs continuously when it can", "[api][always]") { NUClear::PowerPlant::Configuration config; config.thread_count = 1; NUClear::PowerPlant plant(config); + plant.install(); + plant.start(); - // We are installing with an initial log level of debug - plant.install(); - - plant.emit(std::make_unique(5)); + const std::vector expected = { + "Always 0", + "Always 1", + "Always 2", + "Always 3", + "Always 4", + "Always 5", + "Always 6", + "Always 7", + "Always 8", + "Always 9", + "Always with SimpleMessage 10", + }; - plant.start(); + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); - REQUIRE(emitted_message); - REQUIRE(i == 12); + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/ArgumentFission.cpp b/tests/dsl/ArgumentFission.cpp index f358d0886..6b59acab8 100644 --- a/tests/dsl/ArgumentFission.cpp +++ b/tests/dsl/ArgumentFission.cpp @@ -15,114 +15,70 @@ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ - #include #include #include +#include "test_util/TestBase.hpp" + namespace { -struct BindExtensionTest1 { - static int val1; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - static double val2; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +struct BindExtensionTest1 { template - static inline int bind(const std::shared_ptr& /*unused*/, int v1, double v2) { - - val1 = v1; - val2 = v2; - + static inline int bind(const std::shared_ptr& /*unused*/, + const int& v1, + const bool& v2) { + events.push_back("Bind1 with " + std::to_string(v1) + " and " + (v2 ? "true" : "false") + " called"); return 5; } }; -int BindExtensionTest1::val1 = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -double BindExtensionTest1::val2 = 0.0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - struct BindExtensionTest2 { - - static std::string val1; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - static std::chrono::nanoseconds val2; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - template - static inline double bind(const std::shared_ptr& /*reaction*/, - std::string v1, - std::chrono::nanoseconds v2) { - - val1 = std::move(v1); - val2 = v2; - - return 7.2; + static inline bool bind(const std::shared_ptr& /*reaction*/, + const std::string& v1, + const std::chrono::nanoseconds& v2) { + events.push_back("Bind2 with " + v1 + " and " + std::to_string(v2.count()) + " called"); + return true; } }; -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -std::string BindExtensionTest2::val1 = {}; -// NOLINTNEXTLINE(cert-err58-cpp,cppcoreguidelines-avoid-non-const-global-variables) -std::chrono::nanoseconds BindExtensionTest2::val2 = std::chrono::nanoseconds(0); - struct BindExtensionTest3 { - - static int val1; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - static int val2; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - static int val3; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - template - static inline NUClear::threading::ReactionHandle - bind(const std::shared_ptr& /*reaction*/, int v1, int v2, int v3) { - - val1 = v1; - val2 = v2; - val3 = v3; - - return {nullptr}; + static inline std::string bind(const std::shared_ptr& /*reaction*/, + const int& v1, + const int& v2, + const std::chrono::nanoseconds& v3) { + events.push_back("Bind3 with " + std::to_string(v1) + ", " + std::to_string(v2) + " and " + + std::to_string(v3.count()) + " called"); + return "return from Bind3"; } }; -int BindExtensionTest3::val1 = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -int BindExtensionTest3::val2 = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -int BindExtensionTest3::val3 = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -struct ShutdownFlag {}; - -class TestReactor : public NUClear::Reactor { +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { - int a = 0; - double b = 0.0; + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { + int a = 0; + bool b = false; + std::string c; - // Run all three of our extension tests - std::tie(std::ignore, a, b, std::ignore) = + // Bind all three functions to test fission + std::tie(std::ignore, a, b, c) = on(5, - 7.9, + false, "Hello", std::chrono::seconds(2), 9, 10, - 11) + std::chrono::seconds(11)) .then([] {}); - // Check the returns from the bind - REQUIRE(a == 5); - REQUIRE(b == 7.2); - - REQUIRE(BindExtensionTest1::val1 == 5); - REQUIRE(BindExtensionTest1::val2 == 7.9); - - REQUIRE(BindExtensionTest2::val1 == "Hello"); - REQUIRE(BindExtensionTest2::val2.count() == std::chrono::nanoseconds(2 * std::nano::den).count()); - - REQUIRE(BindExtensionTest3::val1 == 9); - REQUIRE(BindExtensionTest3::val2 == 10); - REQUIRE(BindExtensionTest3::val3 == 11); - - // Run a test when there are blanks in the list before filled elements - on, BindExtensionTest1>(2, 3.3).then([] {}); - - on>().then([this] { - // We are finished the test - powerplant.shutdown(); - }); + events.push_back("Bind1 returned " + std::to_string(a)); + events.push_back(std::string("Bind2 returned ") + (b ? "true" : "false")); + events.push_back("Bind3 returned " + c); } }; } // namespace @@ -133,8 +89,20 @@ TEST_CASE("Testing distributing arguments to multiple bind functions (NUClear Fi config.thread_count = 1; NUClear::PowerPlant plant(config); plant.install(); + plant.start(); - plant.emit(std::make_unique()); + const std::vector expected = { + "Bind1 with 5 and false called", + "Bind2 with Hello and 2000000000 called", + "Bind3 with 9, 10 and 11000000000 called", + "Bind1 returned 5", + "Bind2 returned true", + "Bind3 returned return from Bind3", + }; - plant.start(); + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/BlockNoData.cpp b/tests/dsl/BlockNoData.cpp index 59b86401b..807c96113 100644 --- a/tests/dsl/BlockNoData.cpp +++ b/tests/dsl/BlockNoData.cpp @@ -19,49 +19,66 @@ #include #include +#include "test_util/TestBase.hpp" + namespace { -struct SimpleMessage {}; + +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) struct MessageA {}; struct MessageB {}; -MessageA* a = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -class TestReactor : public NUClear::Reactor { +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { + on>().then([this] { + events.push_back("MessageA triggered"); + events.push_back("Emitting MessageB"); + emit(std::make_unique()); + }); - on>().then([this] { - auto data = std::make_unique(); - a = data.get(); + on, With>().then([] { // + events.push_back("MessageA with MessageB triggered"); + }); - // Emit required data - emit(data); + on, With>().then([] { // + events.push_back("MessageB with MessageA triggered"); + }); - // Since the data was emitted before the shutdown call it will be processed before total shutdown - powerplant.shutdown(); + on>, Priority::LOW>().then([this] { + events.push_back("Emitting MessageA"); + emit(std::make_unique()); }); - on, With>().then( - [](const MessageA&, const MessageB&) { FAIL("B was never emitted so this should not be possible"); }); + on().then([this] { + // Emit some messages with data + emit(std::make_unique>()); + }); } }; } // namespace -TEST_CASE("Testing that when a trigger does not have it's data satisfied it does not run", "[api][nodata]") { +TEST_CASE("Testing that when an on statement does not have it's data satisfied it does not run", "[api][nodata]") { NUClear::PowerPlant::Configuration config; config.thread_count = 1; NUClear::PowerPlant plant(config); plant.install(); + plant.start(); - auto message = std::make_unique(); - - plant.emit(message); + const std::vector expected = { + "Emitting MessageA", + "MessageA triggered", + "Emitting MessageB", + "MessageB with MessageA triggered", + }; - plant.start(); + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); - REQUIRE(a != nullptr); + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/Buffer.cpp b/tests/dsl/Buffer.cpp new file mode 100644 index 000000000..200a5fc76 --- /dev/null +++ b/tests/dsl/Buffer.cpp @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2013 Trent Houliston , Jake Woods + * 2014-2017 Trent Houliston + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include + +#include "test_util/TestBase.hpp" + +namespace { + +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +struct Message { + Message(int i) : i(i) {} + int i; +}; + +class TestReactor : public test_util::TestBase { +public: + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { + + on>().then([](const Message& msg) { // + events.push_back("Trigger reaction " + std::to_string(msg.i)); + }); + on, Single>().then([](const Message& msg) { // + events.push_back("Single reaction " + std::to_string(msg.i)); + }); + on, Buffer<2>>().then([](const Message& msg) { // + events.push_back("Buffer<2> reaction " + std::to_string(msg.i)); + }); + on, Buffer<3>>().then([](const Message& msg) { // + events.push_back("Buffer<3> reaction " + std::to_string(msg.i)); + }); + on, Buffer<4>>().then([](const Message& msg) { // + events.push_back("Buffer<4> reaction " + std::to_string(msg.i)); + }); + + on>, Priority::LOW>().then([this] { + events.push_back("Step 1"); + emit(std::make_unique(1)); + }); + on>, Priority::LOW>().then([this] { + events.push_back("Step 2"); + emit(std::make_unique(2)); + emit(std::make_unique(3)); + }); + on>, Priority::LOW>().then([this] { + events.push_back("Step 3"); + emit(std::make_unique(4)); + emit(std::make_unique(5)); + emit(std::make_unique(6)); + }); + on>, Priority::LOW>().then([this] { + events.push_back("Step 4"); + emit(std::make_unique(7)); + emit(std::make_unique(8)); + emit(std::make_unique(9)); + emit(std::make_unique(10)); + }); + on>, Priority::LOW>().then([this] { + events.push_back("Step 5"); + emit(std::make_unique(11)); + emit(std::make_unique(12)); + emit(std::make_unique(13)); + emit(std::make_unique(14)); + emit(std::make_unique(15)); + }); + + on().then("Startup", [this]() { + emit(std::make_unique>()); + emit(std::make_unique>()); + emit(std::make_unique>()); + emit(std::make_unique>()); + emit(std::make_unique>()); + }); + } +}; +} // namespace + +TEST_CASE("Test that Buffer and Single limit the number of concurrent executions", "[api][precondition][single]") { + + NUClear::PowerPlant::Configuration config; + config.thread_count = 1; + NUClear::PowerPlant plant(config); + plant.install(); + plant.start(); + + const std::vector expected = { + "Step 1", + "Trigger reaction 1", + "Single reaction 1", + "Buffer<2> reaction 1", + "Buffer<3> reaction 1", + "Buffer<4> reaction 1", + "Step 2", + "Trigger reaction 2", + "Single reaction 2", + "Buffer<2> reaction 2", + "Buffer<3> reaction 2", + "Buffer<4> reaction 2", + "Trigger reaction 3", + "Buffer<2> reaction 3", + "Buffer<3> reaction 3", + "Buffer<4> reaction 3", + "Step 3", + "Trigger reaction 4", + "Single reaction 4", + "Buffer<2> reaction 4", + "Buffer<3> reaction 4", + "Buffer<4> reaction 4", + "Trigger reaction 5", + "Buffer<2> reaction 5", + "Buffer<3> reaction 5", + "Buffer<4> reaction 5", + "Trigger reaction 6", + "Buffer<3> reaction 6", + "Buffer<4> reaction 6", + "Step 4", + "Trigger reaction 7", + "Single reaction 7", + "Buffer<2> reaction 7", + "Buffer<3> reaction 7", + "Buffer<4> reaction 7", + "Trigger reaction 8", + "Buffer<2> reaction 8", + "Buffer<3> reaction 8", + "Buffer<4> reaction 8", + "Trigger reaction 9", + "Buffer<3> reaction 9", + "Buffer<4> reaction 9", + "Trigger reaction 10", + "Buffer<4> reaction 10", + "Step 5", + "Trigger reaction 11", + "Single reaction 11", + "Buffer<2> reaction 11", + "Buffer<3> reaction 11", + "Buffer<4> reaction 11", + "Trigger reaction 12", + "Buffer<2> reaction 12", + "Buffer<3> reaction 12", + "Buffer<4> reaction 12", + "Trigger reaction 13", + "Buffer<3> reaction 13", + "Buffer<4> reaction 13", + "Trigger reaction 14", + "Buffer<4> reaction 14", + "Trigger reaction 15", + }; + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); +} diff --git a/tests/dsl/CommandLineArguments.cpp b/tests/dsl/CommandLineArguments.cpp index 1a9f7955b..0901c1262 100644 --- a/tests/dsl/CommandLineArguments.cpp +++ b/tests/dsl/CommandLineArguments.cpp @@ -18,27 +18,29 @@ #include #include +#include + +#include "test_util/TestBase.hpp" namespace { -struct ShutdownNowPlx {}; +// Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +class TestReactor : public test_util::TestBase { +private: + using CommandLineArguments = NUClear::message::CommandLineArguments; -class TestReactor : public NUClear::Reactor { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { - on>().then( - [this](const NUClear::message::CommandLineArguments& args) { - REQUIRE(args[0] == "Hello"); - REQUIRE(args[1] == "World"); - - // We can't call shutdown here because - // we haven't started yet. That's because - // emits from Scope::INITIALIZE are not - // considered fully "initialized" - emit(std::make_unique()); - }); - - on>().then([this] { powerplant.shutdown(); }); + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { + + on>().then([](const CommandLineArguments& args) { + std::stringstream output; + for (const auto& arg : args) { + output << arg << " "; + } + events.push_back("CommandLineArguments: " + output.str()); + }); } }; } // namespace @@ -46,11 +48,17 @@ class TestReactor : public NUClear::Reactor { TEST_CASE("Testing the Command Line argument capturing", "[api][command_line_arguments]") { const int argc = 2; const char* argv[] = {"Hello", "World"}; // NOLINT(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays) - NUClear::PowerPlant::Configuration config; config.thread_count = 1; - NUClear::PowerPlant plant(config, argc, reinterpret_cast(argv)); + NUClear::PowerPlant plant(config, argc, argv); plant.install(); - plant.start(); + + const std::vector expected = {"CommandLineArguments: Hello World "}; + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/CustomGet.cpp b/tests/dsl/CustomGet.cpp index a0b48a3c0..a588a469c 100644 --- a/tests/dsl/CustomGet.cpp +++ b/tests/dsl/CustomGet.cpp @@ -19,29 +19,34 @@ #include #include +#include "test_util/TestBase.hpp" + namespace { -struct CustomGet : public NUClear::dsl::operation::TypeBind { +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +struct CustomGet : public NUClear::dsl::operation::TypeBind { template - static inline std::shared_ptr get(NUClear::threading::Reaction& /*unused*/) { - return std::make_shared(5); + static inline std::shared_ptr get(NUClear::threading::Reaction& /*unused*/) { + return std::make_shared("Data from a custom getter"); } }; -class TestReactor : public NUClear::Reactor { +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { - on().then([this](const int& x) { - REQUIRE(x == 5); - - powerplant.shutdown(); + on().then([](const std::string& x) { // + events.push_back("CustomGet Triggered"); + events.push_back(x); }); on().then([this] { - // Emit from message 4 to 1 - emit(std::make_unique(10)); + // Emit a CustomGet instance to trigger the reaction + events.push_back("Emitting CustomGet"); + emit(std::make_unique()); }); } }; @@ -53,6 +58,17 @@ TEST_CASE("Test a custom reactor that returns a type that needs dereferencing", config.thread_count = 1; NUClear::PowerPlant plant(config); plant.install(); - plant.start(); + + const std::vector expected = { + "Emitting CustomGet", + "CustomGet Triggered", + "Data from a custom getter", + }; + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/DSLOrdering.cpp b/tests/dsl/DSLOrdering.cpp new file mode 100644 index 000000000..c8bd279cb --- /dev/null +++ b/tests/dsl/DSLOrdering.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2013 Trent Houliston , Jake Woods + * 2014-2017 Trent Houliston + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include + +#include "test_util/TestBase.hpp" + +namespace { + +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +template +struct Message { + Message(std::string data) : data(std::move(data)) {} + std::string data; +}; + +class TestReactor : public test_util::TestBase { +public: + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { + // Check that the lists are combined, and that the function args are in order + on>, Trigger>, With>>().then( + [](const Message<1>& a, const Message<3>& c, const Message<2>& b) { + events.push_back("A:" + a.data + " B:" + b.data + " C:" + c.data); + }); + + // Make sure we can pass an empty function in here + on>, With, Message<2>>>().then([] { events.push_back("Empty function"); }); + + on>, Priority::LOW>().then([this] { + events.push_back("Emitting 1"); + emit(std::make_unique>("1")); + }); + on>, Priority::LOW>().then([this] { + events.push_back("Emitting 2"); + emit(std::make_unique>("2")); + }); + on>, Priority::LOW>().then([this] { + events.push_back("Emitting 3"); + emit(std::make_unique>("3")); + }); + + on().then([this] { + emit(std::make_unique>()); + emit(std::make_unique>()); + emit(std::make_unique>()); + }); + } +}; +} // namespace + +TEST_CASE("Testing poorly ordered on arguments", "[api][dsl][order][with]") { + + NUClear::PowerPlant::Configuration config; + config.thread_count = 1; + NUClear::PowerPlant plant(config); + plant.install(); + plant.start(); + + const std::vector expected = { + "Emitting 1", + "Emitting 2", + "Emitting 3", + "A:1 B:2 C:3", + }; + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); +} diff --git a/tests/dsl/DSLProxy.cpp b/tests/dsl/DSLProxy.cpp index 188f4bbd4..f2f3e148c 100644 --- a/tests/dsl/DSLProxy.cpp +++ b/tests/dsl/DSLProxy.cpp @@ -19,13 +19,23 @@ #include #include +#include "test_util/TestBase.hpp" + +namespace { +struct CustomMessage1 {}; +struct CustomMessage2 { + CustomMessage2(int value) : value(value) {} + int value; +}; +} // namespace + namespace NUClear { namespace dsl { namespace operation { template <> - struct DSLProxy - : public NUClear::dsl::operation::TypeBind - , public NUClear::dsl::operation::CacheGet + struct DSLProxy + : public NUClear::dsl::operation::TypeBind + , public NUClear::dsl::operation::CacheGet , public NUClear::dsl::word::Single {}; } // namespace operation } // namespace dsl @@ -33,24 +43,25 @@ namespace dsl { namespace { -class TestReactor : public NUClear::Reactor { -public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - on().then([this](const double& d) { - // The message we received should have test == 10 - REQUIRE(d == 4.4); +class TestReactor : public test_util::TestBase { +public: + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { - // We are finished the test - powerplant.shutdown(); + on().then([](const CustomMessage2& d) { + events.push_back("CustomMessage1 Triggered with " + std::to_string(d.value)); }); on().then([this]() { // Emit a double we can get - emit(std::make_unique(4.4)); + events.push_back("Emitting CustomMessage2"); + emit(std::make_unique(123456)); - // Emit an integer to trigger the reaction - emit(std::make_unique()); + // Emit a custom message 1 to trigger the reaction + events.push_back("Emitting CustomMessage1"); + emit(std::make_unique()); }); } }; @@ -62,6 +73,17 @@ TEST_CASE("Testing that the DSL proxy works as expected for binding unmodifyable config.thread_count = 1; NUClear::PowerPlant plant(config); plant.install(); - plant.start(); + + const std::vector expected = { + "Emitting CustomMessage2", + "Emitting CustomMessage1", + "CustomMessage1 Triggered with 123456", + }; + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/Every.cpp b/tests/dsl/Every.cpp index 50cfb2bbe..982a04eeb 100644 --- a/tests/dsl/Every.cpp +++ b/tests/dsl/Every.cpp @@ -20,70 +20,75 @@ #include #include -namespace { - -class TestReactor : public NUClear::Reactor { -public: - // Store our times - std::vector times{}; +#include "test_util/TestBase.hpp" - static constexpr size_t NUM_LOG_ITEMS = 1000; +namespace { - static constexpr size_t WAIT_LENGTH_MILLIS = 1; +// Store our times +std::vector every_times{}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +std::vector per_times{}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +std::vector dynamic_times{}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { +class TestReactor : public test_util::TestBase { - // Trigger every 10 milliseconds - on>().then([this] { - // Start logging our times each time an emit happens - times.push_back(NUClear::clock::now()); +public: + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment), false) { - // Once we have enough items then we can do our statistics - if (times.size() == NUM_LOG_ITEMS) { + // Trigger on 3 different types of every + on>>().then([]() { every_times.push_back(NUClear::clock::now()); }); + on>().then([]() { per_times.push_back(NUClear::clock::now()); }); + on>(std::chrono::milliseconds(1)).then([]() { dynamic_times.push_back(NUClear::clock::now()); }); - // Build up our difference vector - std::vector diff; + // Gather data for some amount of time + on>().then([this] { powerplant.shutdown(); }); + } +}; +} // namespace - for (size_t i = 0; i < times.size() - 1; ++i) { - const std::chrono::nanoseconds delta = times[i + 1] - times[i]; +void test_results(const std::vector& times) { - // Store our difference in seconds - diff.push_back(double(delta.count()) / double(std::nano::den)); - } + // Build up our difference vector + std::vector diff; + for (size_t i = 0; i < times.size() - 1; ++i) { + const double delta = std::chrono::duration_cast>(times[i + 1] - times[i]).count(); - // Normalize our differences to jitter - for (double& d : diff) { - d -= double(WAIT_LENGTH_MILLIS) / 1000.0; - } + // Calculate the difference between the expected and actual time + diff.push_back(delta - 1e-3); + } - // Calculate our mean, range, and stddev for the set - const double sum = std::accumulate(std::begin(diff), std::end(diff), 0.0); - const double mean = sum / double(diff.size()); - const double variance = std::inner_product(diff.begin(), diff.end(), diff.begin(), 0.0); - const double stddev = std::sqrt(variance / double(diff.size())); + // Calculate our mean, range, and stddev for the set + const double sum = std::accumulate(std::begin(diff), std::end(diff), 0.0); + const double mean = sum / double(diff.size()); + const double variance = std::inner_product(diff.begin(), diff.end(), diff.begin(), 0.0); + const double stddev = std::sqrt(variance / double(diff.size() - 1)); - // As time goes on the average wait should be 0 (we accept less then 0.5ms for this test) - REQUIRE(fabs(mean) < 0.0005); + // As time goes on the average wait should be close to 0 + INFO("Average error in timing: " << mean << "±" << stddev); - // Require that 95% (ish) of all results are fast enough - REQUIRE(fabs(mean + stddev * 2) < 0.008); - } - // Once we have more then enough items then we shutdown the powerplant - else if (times.size() > NUM_LOG_ITEMS) { - // We are finished the test - this->powerplant.shutdown(); - } - }); - } -}; -} // namespace + REQUIRE(std::abs(mean) < 0.0005); + REQUIRE(std::abs(mean + stddev * 2) < 0.008); +} -TEST_CASE("Testing the Every<> Smart Type", "[api][every][period]") { +TEST_CASE("Testing the Every<> DSL word", "[api][every][per]") { NUClear::PowerPlant::Configuration config; config.thread_count = 1; NUClear::PowerPlant plant(config); plant.install(); - plant.start(); + + { + INFO("Testing Every"); + test_results(every_times); + } + + { + INFO("Testing Every Per"); + test_results(per_times); + } + + { + INFO("Testing Dynamic Every every"); + test_results(dynamic_times); + } } diff --git a/tests/dsl/Every_Per.cpp b/tests/dsl/Every_Per.cpp deleted file mode 100644 index 90e2c35f9..000000000 --- a/tests/dsl/Every_Per.cpp +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2013 Trent Houliston , Jake Woods - * 2014-2017 Trent Houliston - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the - * Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE - * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR - * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -#include -#include -#include - -namespace { - -class TestReactorPer : public NUClear::Reactor { -public: - // Store our times - std::vector times{}; - - static constexpr size_t NUM_LOG_ITEMS = 1000; - - static constexpr size_t CYCLES_PER_SECOND = 1000; - - TestReactorPer(std::unique_ptr environment) : Reactor(std::move(environment)) { - - // Trigger every 10 milliseconds - on>>().then([this]() { - // Start logging our times each time an emit happens - times.push_back(NUClear::clock::now()); - - // Once we have enough items then we can do our statistics - if (times.size() == NUM_LOG_ITEMS) { - - // Build up our difference vector - std::vector diff; - - for (size_t i = 0; i < times.size() - 1; ++i) { - const std::chrono::nanoseconds delta = times[i + 1] - times[i]; - - // Store our difference in seconds - diff.push_back(double(delta.count()) / double(std::nano::den)); - } - - // Normalize our differences to jitter - for (double& d : diff) { - d -= 1.0 / double(CYCLES_PER_SECOND); - } - - // Calculate our mean, range, and stddev for the set - const double sum = std::accumulate(std::begin(diff), std::end(diff), 0.0); - const double mean = sum / double(diff.size()); - const double variance = std::inner_product(diff.begin(), diff.end(), diff.begin(), 0.0); - const double stddev = std::sqrt(variance / double(diff.size())); - - // As time goes on the average wait should be 0 (we accept less then 0.5ms for this test) - REQUIRE(fabs(mean) < 0.0005); - - // Require that 95% (ish) of all results are fast enough - REQUIRE(fabs(mean + stddev * 2) < 0.008); - } - // Once we have more then enough items then we shutdown the powerplant - else if (times.size() > NUM_LOG_ITEMS) { - // We are finished the test - this->powerplant.shutdown(); - } - }); - } -}; -} // namespace - -TEST_CASE("Testing the Every<> Smart Type using Per", "[api][every][per]") { - - NUClear::PowerPlant::Configuration config; - config.thread_count = 1; - NUClear::PowerPlant plant(config); - plant.install(); - - plant.start(); -} diff --git a/tests/dsl/FlagMessage.cpp b/tests/dsl/FlagMessage.cpp index db333ce87..d9930b7b1 100644 --- a/tests/dsl/FlagMessage.cpp +++ b/tests/dsl/FlagMessage.cpp @@ -19,50 +19,47 @@ #include #include +#include "test_util/TestBase.hpp" + namespace { + +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + struct SimpleMessage {}; struct MessageA {}; struct MessageB {}; -MessageA* a = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -MessageB* b = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -class TestReactor : public NUClear::Reactor { +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { - - - on>().then([this] { - auto data = std::make_unique(); - a = data.get(); + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { - // Emit the first half of the requred data - emit(data); - }); on>().then([this] { - // Check a has been emitted - REQUIRE(a != nullptr); - - auto data = std::make_unique(); - b = data.get(); + events.push_back("MessageA triggered"); + events.push_back("Emitting MessageB"); + emit(std::make_unique()); + }); - // Emit the 2nd half - emit(data); + on>().then([] { // + events.push_back("MessageB triggered"); + }); - // We can shutdown now, the other reactions will process before termination - powerplant.shutdown(); + // This should never run + on, With>().then([](const MessageA&, const MessageB&) { // + events.push_back("MessageA with MessageB triggered"); }); - on>().then([] { - // Check b has been emitted - REQUIRE(b != nullptr); + on>, Priority::LOW>().then([this] { + events.push_back("Step<1> triggered"); + events.push_back("Emitting MessageA"); + emit(std::make_unique()); }); - // We make this high priority to ensure it runs first (will check for more errors) - on, With, Priority::HIGH>().then([](const MessageA&, const MessageB&) { - FAIL("A was never emitted after B so this should not be possible"); + on().then([this] { + events.push_back("Emitting Step<1>"); + emit(std::make_unique>()); }); } }; @@ -75,10 +72,20 @@ TEST_CASE("Testing emitting types that are flag types (Have no contents)", "[api config.thread_count = 1; NUClear::PowerPlant plant(config); plant.install(); + plant.start(); - auto message = std::make_unique(); + const std::vector expected = { + "Emitting Step<1>", + "Step<1> triggered", + "Emitting MessageA", + "MessageA triggered", + "Emitting MessageB", + "MessageB triggered", + }; - plant.emit(message); + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); - plant.start(); + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/IO.cpp b/tests/dsl/IO.cpp index 165b16c8f..7e7b9ae28 100644 --- a/tests/dsl/IO.cpp +++ b/tests/dsl/IO.cpp @@ -19,6 +19,8 @@ #include #include +#include "test_util/TestBase.hpp" + // Windows can't do this test as it doesn't have file descriptors #ifndef _WIN32 @@ -28,49 +30,66 @@ namespace { -class TestReactor : public NUClear::Reactor { +/// @brief Events that occur during the test reading +std::vector read_events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +/// @brief Events that occur during the test writing +std::vector write_events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment), false) { std::array fds{-1, -1}; - if (pipe(fds.data()) < 0) { - FAIL("We couldn't make the pipe for the test"); + if (::pipe(fds.data()) < 0) { + return; } - in = fds[0]; out = fds[1]; - on(in, IO::READ).then([this](const IO::Event& e) { - // Read from our FD - unsigned char val{0}; - const ssize_t bytes = ::read(e.fd, &val, 1); - - // Check the data is correct - REQUIRE((e.events & IO::READ) != 0); - REQUIRE(bytes == 1); - REQUIRE(val == 0xDE); - - // Shutdown - powerplant.shutdown(); + // Set the pipe to non-blocking + ::fcntl(in.get(), F_SETFL, ::fcntl(in.get(), F_GETFL) | O_NONBLOCK); + ::fcntl(out.get(), F_SETFL, ::fcntl(out.get(), F_GETFL) | O_NONBLOCK); + + on(in.get(), IO::READ | IO::CLOSE).then([this](const IO::Event& e) { + if ((e.events & IO::READ) != 0) { + // Read from our fd + char c{0}; + for (auto bytes = ::read(e.fd, &c, 1); bytes > 0; bytes = ::read(e.fd, &c, 1)) { + // If we read something, log it + if (bytes > 0) { + read_events.push_back("Read " + std::to_string(bytes) + " bytes (" + c + ") from pipe"); + } + } + } + + // FD was closed + if ((e.events & IO::CLOSE) != 0) { + read_events.push_back("Closed pipe"); + powerplant.shutdown(); + } }); - writer = on(out, IO::WRITE).then([this](const IO::Event& e) { + writer = on(out.get(), IO::WRITE).then([this](const IO::Event& e) { // Send data into our fd - const unsigned char val = 0xDE; - const ssize_t bytes = ::write(e.fd, &val, 1); - - // Check that our data was sent - REQUIRE((e.events & IO::WRITE) != 0); - REQUIRE(bytes == 1); - - // Unbind ourselves - writer.unbind(); + const char c = "Hello"[char_no]; + const ssize_t sent = ::write(e.fd, &c, 1); + + // If we wrote something, log it and move to the next character + if (sent > 0) { + ++char_no; + write_events.push_back("Wrote " + std::to_string(sent) + " bytes (" + c + ") to pipe"); + } + + if (char_no == 5) { + ::close(e.fd); + } }); } - int in{-1}; - int out{-1}; + NUClear::util::FileDescriptor in{}; + NUClear::util::FileDescriptor out{}; + int char_no{0}; ReactionHandle writer{}; }; } // namespace @@ -81,8 +100,34 @@ TEST_CASE("Testing the IO extension", "[api][io]") { config.thread_count = 1; NUClear::PowerPlant plant(config); plant.install(); - plant.start(); + + const std::vector read_expected = { + "Read 1 bytes (H) from pipe", + "Read 1 bytes (e) from pipe", + "Read 1 bytes (l) from pipe", + "Read 1 bytes (l) from pipe", + "Read 1 bytes (o) from pipe", + "Closed pipe", + }; + + // Make an info print the diff in an easy to read way if we fail + INFO("Read Events\n" << test_util::diff_string(read_expected, read_events)); + + const std::vector write_expected{ + "Wrote 1 bytes (H) to pipe", + "Wrote 1 bytes (e) to pipe", + "Wrote 1 bytes (l) to pipe", + "Wrote 1 bytes (l) to pipe", + "Wrote 1 bytes (o) to pipe", + }; + + // Make an info print the diff in an easy to read way if we fail + INFO("Write Events\n" << test_util::diff_string(write_expected, write_events)); + + // Check the events fired in order and only those events + REQUIRE(read_events == read_expected); + REQUIRE(write_events == write_expected); } -#endif +#endif // _WIN32 diff --git a/tests/dsl/Last.cpp b/tests/dsl/Last.cpp index 927bfaf9c..bd951d9d1 100644 --- a/tests/dsl/Last.cpp +++ b/tests/dsl/Last.cpp @@ -19,53 +19,40 @@ #include #include +#include "test_util/TestBase.hpp" + namespace { -struct TestMessage { - int value; +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +struct TestMessage { TestMessage(int v) : value(v){}; + int value; }; -int emit_counter = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -int recv_counter = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -class TestReactor : public NUClear::Reactor { +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { on>>().then([this](std::list> messages) { - // We got another one - ++recv_counter; - - // Send out another before we test - emit(std::make_unique(++emit_counter)); - - // Finish when we get to 10 - if (messages.front()->value >= 10) { - powerplant.shutdown(); + std::stringstream ss; + for (auto& m : messages) { + ss << m->value << " "; } - else { - // Our list must be less than 5 long - REQUIRE(messages.size() <= 5); - - // If our size is less than 5 it should be the size of the front element - if (messages.size() < 5) { - REQUIRE(int(messages.size()) == messages.back()->value); - } + events.push_back(ss.str()); - // Check that our numbers are increasing - int i = messages.front()->value; - for (auto& m : messages) { - REQUIRE(m->value == i); - ++i; - } + // Finish when we get to 10 + if (messages.back()->value < 10) { + // Send out another message + emit(std::make_unique(messages.back()->value + 1)); } }); - on().then([this] { emit(std::make_unique(++emit_counter)); }); + on().then([this] { emit(std::make_unique(0)); }); } }; + } // namespace TEST_CASE("Testing the last n feature", "[api][last]") { @@ -74,6 +61,25 @@ TEST_CASE("Testing the last n feature", "[api][last]") { config.thread_count = 1; NUClear::PowerPlant plant(config); plant.install(); - plant.start(); + + const std::vector expected = { + "0 ", + "0 1 ", + "0 1 2 ", + "0 1 2 3 ", + "0 1 2 3 4 ", + "1 2 3 4 5 ", + "2 3 4 5 6 ", + "3 4 5 6 7 ", + "4 5 6 7 8 ", + "5 6 7 8 9 ", + "6 7 8 9 10 ", + }; + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/MainThread.cpp b/tests/dsl/MainThread.cpp index b7f1d1ec6..adb87f803 100644 --- a/tests/dsl/MainThread.cpp +++ b/tests/dsl/MainThread.cpp @@ -19,43 +19,66 @@ #include #include +#include "test_util/TestBase.hpp" + namespace { -class TestReactor : public NUClear::Reactor { +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +struct MessageA {}; +struct MessageB {}; + +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment), false) { // Run a task without MainThread to make sure it isn't on the main thread - on>().then("Non-MainThread reaction", [this] { - // We shouldn't be on the main thread - REQUIRE(NUClear::util::main_thread_id != std::this_thread::get_id()); + on>().then("Non-MainThread reaction", [this] { + events.push_back(std::string("MessageA triggered ") + + (NUClear::util::main_thread_id == std::this_thread::get_id() ? "on main thread" + : "on non-main thread")); - emit(std::make_unique(1.1)); + events.push_back("Emitting MessageB"); + emit(std::make_unique()); }); // Run a task with MainThread and ensure that it is on the main thread - on, MainThread>().then("MainThread reaction", [this] { - // Shutdown first so the test will end even if the next check fails - powerplant.shutdown(); + on, MainThread>().then("MainThread reaction", [this] { + events.push_back(std::string("MessageB triggered ") + + (NUClear::util::main_thread_id == std::this_thread::get_id() ? "on main thread" + : "on non-main thread")); - // We should be on the main thread - REQUIRE(NUClear::util::main_thread_id == std::this_thread::get_id()); + // Since we are a multithreaded test with MainThread we need to shutdown the test ourselves + powerplant.shutdown(); }); on().then([this]() { // Emit an integer to trigger the reaction - emit(std::make_unique()); + events.push_back("Emitting MessageA"); + emit(std::make_unique()); }); } }; } // namespace TEST_CASE("Testing that the MainThread keyword runs tasks on the main thread", "[api][dsl][main_thread]") { - NUClear::PowerPlant::Configuration config; config.thread_count = 1; NUClear::PowerPlant plant(config); plant.install(); - plant.start(); + + const std::vector expected = { + "Emitting MessageA", + "MessageA triggered on non-main thread", + "Emitting MessageB", + "MessageB triggered on main thread", + }; + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/MissingArguments.cpp b/tests/dsl/MissingArguments.cpp index e05513db5..230085156 100644 --- a/tests/dsl/MissingArguments.cpp +++ b/tests/dsl/MissingArguments.cpp @@ -19,32 +19,39 @@ #include #include +#include "test_util/TestBase.hpp" + namespace { +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + template struct Message { int val; Message(int val) : val(val){}; }; -class TestReactor : public NUClear::Reactor { +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { on>, With>, With>, With>>().then( - [this](const Message<2>& m2, const Message<4>& m4) { - REQUIRE(m2.val == 2 + 4); - REQUIRE(m4.val == 4 + 4); - - powerplant.shutdown(); + [](const Message<2>& m2, const Message<4>& m4) { + events.push_back("Message<2>: " + std::to_string(m2.val)); + events.push_back("Message<4>: " + std::to_string(m4.val)); }); on().then([this] { // Emit from message 4 to 1 - emit(std::make_unique>(8)); - emit(std::make_unique>(7)); - emit(std::make_unique>(6)); - emit(std::make_unique>(5)); + events.push_back("Emitting Message<4>"); + emit(std::make_unique>(4 * 4)); + events.push_back("Emitting Message<3>"); + emit(std::make_unique>(3 * 3)); + events.push_back("Emitting Message<2>"); + emit(std::make_unique>(2 * 2)); + events.push_back("Emitting Message<1>"); + emit(std::make_unique>(1 * 1)); }); } }; @@ -56,6 +63,20 @@ TEST_CASE("Testing that when arguments missing from the call it can still run", config.thread_count = 1; NUClear::PowerPlant plant(config); plant.install(); - plant.start(); + + const std::vector expected = { + "Emitting Message<4>", + "Emitting Message<3>", + "Emitting Message<2>", + "Emitting Message<1>", + "Message<2>: 4", + "Message<4>: 16", + }; + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/Once.cpp b/tests/dsl/Once.cpp index 08d6b0930..772d8019d 100644 --- a/tests/dsl/Once.cpp +++ b/tests/dsl/Once.cpp @@ -17,53 +17,75 @@ */ #include -#include #include +#include "test_util/TestBase.hpp" + namespace { -struct SimpleMessage {}; -struct StartMessage {}; +// Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -int i = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -int j = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +struct SimpleMessage { + SimpleMessage(int run) : run(run) {} + int run = 0; +}; class TestReactor : public NUClear::Reactor { public: TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { // Make this priority high so it will always run first if it is able - on, Priority::HIGH, Once>().then([] { - // Increment the counter, - ++i; - // Function has finished, then should unbind. + on, Priority::HIGH, Once>().then([](const SimpleMessage& msg) { // + events.push_back("Once Trigger executed " + std::to_string(msg.run)); }); - on>().then([this] { - ++j; - // Run until it's 11 then shutdown - if (j > 10) { - powerplant.shutdown(); + on>().then([this](const SimpleMessage& msg) { + events.push_back("Normal Trigger Executed " + std::to_string(msg.run)); + // Keep running until we have run 10 times + if (msg.run < 10) { + events.push_back("Emitting " + std::to_string(msg.run + 1)); + emit(std::make_unique(msg.run + 1)); } else { - powerplant.emit(std::make_unique()); - powerplant.emit(std::make_unique()); + powerplant.shutdown(); } }); + + on().then([this] { + events.push_back("Startup Trigger Executed"); + emit(std::make_unique(0)); + }); } }; } // namespace -TEST_CASE("Testing on functionality", "[api][once]") { +TEST_CASE("Reactions with the Once DSL keyword only execute once", "[api][once]") { NUClear::PowerPlant::Configuration config; config.thread_count = 1; NUClear::PowerPlant plant(config); - - // We are installing with an initial log level of debug - plant.install(); - plant.emit(std::make_unique()); + plant.install(); plant.start(); - REQUIRE(i == 1); + const std::vector expected = { + "Startup Trigger Executed", "Once Trigger executed 0", + "Normal Trigger Executed 0", "Emitting 1", + "Normal Trigger Executed 1", "Emitting 2", + "Normal Trigger Executed 2", "Emitting 3", + "Normal Trigger Executed 3", "Emitting 4", + "Normal Trigger Executed 4", "Emitting 5", + "Normal Trigger Executed 5", "Emitting 6", + "Normal Trigger Executed 6", "Emitting 7", + "Normal Trigger Executed 7", "Emitting 8", + "Normal Trigger Executed 8", "Emitting 9", + "Normal Trigger Executed 9", "Emitting 10", + "Normal Trigger Executed 10", + }; + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/Optional.cpp b/tests/dsl/Optional.cpp index c5ce9fa85..4d86e7e45 100644 --- a/tests/dsl/Optional.cpp +++ b/tests/dsl/Optional.cpp @@ -19,71 +19,45 @@ #include #include +#include "test_util/TestBase.hpp" + namespace { -int trigger1 = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -int trigger2 = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -int trigger3 = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -int trigger4 = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) struct MessageA {}; struct MessageB {}; -class TestReactor : public NUClear::Reactor { +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { - on, With>().then([](const MessageA&, const MessageB&) { - ++trigger1; - FAIL("This should never run as MessageB is never emitted"); + on, With>().then([](const MessageA&, const MessageB&) { // + events.push_back("Executed reaction with A and B"); }); - on, Optional>>().then( - [this](const MessageA&, const std::shared_ptr& b) { - ++trigger2; - - switch (trigger2) { - case 1: - // On our trigger, b should not exist - REQUIRE(!b); - break; - default: FAIL("Trigger 2 was triggered more than once"); - } - - // Emit B to start the second set - emit(std::make_unique()); - }); + on, Optional>>().then([this](const std::shared_ptr& b) { + events.push_back(std::string("Executed reaction with A and optional B with B") + (b ? "+" : "-")); + // Emit B to start the second set + events.push_back("Emitting B"); + emit(std::make_unique()); + }); - on, With>().then([] { - // This should run once - ++trigger3; + on, With>().then([] { // + events.push_back("Executed reaction with B and A"); }); // Double trigger test (to ensure that it can handle multiple DSL words on, Trigger>>().then( - [this](const std::shared_ptr& a, const std::shared_ptr& b) { - ++trigger4; - switch (trigger4) { - case 1: - // Check that A exists and B does not - REQUIRE(a); - REQUIRE(!b); - break; - case 2: - // Check that both exist - REQUIRE(a); - REQUIRE(b); - - // We should be done now - powerplant.shutdown(); - break; - - default: FAIL("Trigger 4 should only be triggered twice"); break; - } + [](const std::shared_ptr& a, const std::shared_ptr& b) { // + events.push_back(std::string("Executed reaction with optional A and B with A") + (a ? "+" : "-") + + " and B" + (b ? "+" : "-")); }); on().then([this] { // Emit only message A + events.push_back("Emitting A"); emit(std::make_unique()); }); } @@ -96,12 +70,20 @@ TEST_CASE("Testing that optional is able to let data through even if it's invali config.thread_count = 1; NUClear::PowerPlant plant(config); plant.install(); - plant.start(); - // Check that it was all as expected - REQUIRE(trigger1 == 0); - REQUIRE(trigger2 == 1); - REQUIRE(trigger3 == 1); - REQUIRE(trigger4 == 2); + const std::vector expected = { + "Emitting A", + "Executed reaction with A and optional B with B-", + "Emitting B", + "Executed reaction with optional A and B with A+ and B-", + "Executed reaction with B and A", + "Executed reaction with optional A and B with A+ and B+", + }; + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/Priority.cpp b/tests/dsl/Priority.cpp index 7a16e1f49..d1a075dac 100644 --- a/tests/dsl/Priority.cpp +++ b/tests/dsl/Priority.cpp @@ -19,48 +19,64 @@ #include #include +#include "test_util/TestBase.hpp" + namespace { -struct Message1 {}; -struct Message2 {}; -struct Message3 {}; +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -bool low = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -bool med = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -bool high = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +template +struct Message {}; -class TestReactor : public NUClear::Reactor { +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { - - on, Priority::HIGH>().then("High", [] { - // We should be the first to run - REQUIRE(!low); - REQUIRE(!med); - REQUIRE(!high); - - high = true; - }); - - on, Priority::NORMAL>().then("Normal", [] { - // We should be the second to run - REQUIRE(!low); - REQUIRE(!med); - REQUIRE(high); - - med = true; - }); - - on, Priority::LOW>().then("Low", [this] { - // We should be the final one to run - REQUIRE(!low); - REQUIRE(med); - REQUIRE(high); - - low = true; - - // We're done - powerplant.shutdown(); + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { + + // Declare in the order you'd expect them to fire + on>, Priority::REALTIME>().then([] { events.push_back("Realtime Message<1>"); }); + on>, Priority::HIGH>().then("High", [] { events.push_back("High Message<1>"); }); + on>>().then([] { events.push_back("Default Message<1>"); }); + on>, Priority::NORMAL>().then("Normal", [] { events.push_back("Normal Message<1>"); }); + on>, Priority::LOW>().then("Low", [] { events.push_back("Low Message<1>"); }); + on>, Priority::IDLE>().then([] { events.push_back("Idle Message<1>"); }); + + // Declare in the opposite order to what you'd expect them to fire + on>, Priority::IDLE>().then([] { events.push_back("Idle Message<2>"); }); + on>, Priority::LOW>().then([] { events.push_back("Low Message<2>"); }); + on>, Priority::NORMAL>().then([] { events.push_back("Normal Message<2>"); }); + on>>().then([] { events.push_back("Default Message<2>"); }); + on>, Priority::HIGH>().then([] { events.push_back("High Message<2>"); }); + on>, Priority::REALTIME>().then([] { events.push_back("Realtime Message<2>"); }); + + // Declare in a random order + std::array order = {0, 1, 2, 3, 4}; + std::shuffle(order.begin(), order.end(), std::mt19937(std::random_device()())); + for (const auto& i : order) { + switch (i) { + case 0: + on>, Priority::REALTIME>().then([] { events.push_back("Realtime Message<3>"); }); + break; + case 1: + on>, Priority::HIGH>().then([] { events.push_back("High Message<3>"); }); + break; + case 2: + on>, Priority::NORMAL>().then([] { events.push_back("Normal Message<3>"); }); + on>>().then([] { events.push_back("Default Message<3>"); }); + break; + case 3: + on>, Priority::LOW>().then([] { events.push_back("Low Message<3>"); }); + break; + case 4: + on>, Priority::IDLE>().then([] { events.push_back("Idle Message<3>"); }); + break; + } + } + + on().then([this] { + emit(std::make_unique>()); + emit(std::make_unique>()); + emit(std::make_unique>()); }); } }; @@ -73,17 +89,32 @@ TEST_CASE("Tests that priority orders the tasks appropriately", "[api][priority] config.thread_count = 1; NUClear::PowerPlant plant(config); plant.install(); - - // Emit message 2, then 1 then 3 (totally wrong order) - // Should require the priority queue to sort it out - plant.emit(std::make_unique()); - plant.emit(std::make_unique()); - plant.emit(std::make_unique()); - plant.start(); - // Make sure everything ran - REQUIRE(low); - REQUIRE(med); - REQUIRE(high); + const std::vector expected = { + "Realtime Message<1>", + "Realtime Message<2>", + "Realtime Message<3>", + "High Message<1>", + "High Message<2>", + "High Message<3>", + "Default Message<1>", + "Normal Message<1>", + "Normal Message<2>", + "Default Message<2>", + "Normal Message<3>", + "Default Message<3>", + "Low Message<1>", + "Low Message<2>", + "Low Message<3>", + "Idle Message<1>", + "Idle Message<2>", + "Idle Message<3>", + }; + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/RawFunction.cpp b/tests/dsl/RawFunction.cpp index 7ae93d204..6f12d5cd8 100644 --- a/tests/dsl/RawFunction.cpp +++ b/tests/dsl/RawFunction.cpp @@ -19,22 +19,83 @@ #include #include +#include "test_util/TestBase.hpp" + namespace { -bool ran = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +struct Message { + Message(std::string data) : data(std::move(data)) {} + std::string data; +}; + +struct Data { + Data(std::string data) : data(std::move(data)) {} + std::string data; +}; -double do_amazing_thing() { - ran = true; +/** + * @brief Test a raw function that takes no arguments and has a return type. + * The return type should be ignored and this function should run without issue. + * + * @return double + */ +double raw_function_test_no_args() { + events.push_back("Raw function no args"); return 5.0; } -class TestReactor : public NUClear::Reactor { +/** + * @brief Raw function that takes one argument (the left side of the trigger) + * + * @param msg the message + */ +void raw_function_test_left_arg(const Message& msg) { + events.push_back("Raw function left arg: " + msg.data); +} + +/** + * @brief Raw function that takes one argument (the right side of the trigger) + * + * @param data the data + */ +void raw_function_test_right_arg(const Data& data) { + events.push_back("Raw function right arg: " + data.data); +} + +/** + * @brief Raw function that takes both arguments + * + * @param msg the message + * @param data the data + */ +void raw_function_test_both_args(const Message& msg, const Data& data) { + events.push_back("Raw function both args: " + msg.data + " " + data.data); +} + + +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { + + on, Trigger>().then(raw_function_test_no_args); + on, Trigger>().then(raw_function_test_left_arg); + on, Trigger>().then(raw_function_test_right_arg); + on, Trigger>().then(raw_function_test_both_args); - on().then(do_amazing_thing); + on>, Priority::LOW>().then([this] { emit(std::make_unique("D1")); }); + on>, Priority::LOW>().then([this] { emit(std::make_unique("M2")); }); + on>, Priority::LOW>().then([this] { emit(std::make_unique("D3")); }); + on>, Priority::LOW>().then([this] { emit(std::make_unique("M4")); }); - on().then([this] { powerplant.shutdown(); }); + on().then([this] { + emit(std::make_unique>()); + emit(std::make_unique>()); + emit(std::make_unique>()); + emit(std::make_unique>()); + }); } }; } // namespace @@ -45,8 +106,26 @@ TEST_CASE("Test reaction can take a raw function instead of just a lambda", "[ap config.thread_count = 1; NUClear::PowerPlant plant(config); plant.install(); - plant.start(); - REQUIRE(ran); + const std::vector expected = { + "Raw function no args", + "Raw function left arg: M2", + "Raw function right arg: D1", + "Raw function both args: M2 D1", + "Raw function no args", + "Raw function left arg: M2", + "Raw function right arg: D3", + "Raw function both args: M2 D3", + "Raw function no args", + "Raw function left arg: M4", + "Raw function right arg: D3", + "Raw function both args: M4 D3", + }; + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/Shutdown.cpp b/tests/dsl/Shutdown.cpp index 1a028bd36..11ef295b5 100644 --- a/tests/dsl/Shutdown.cpp +++ b/tests/dsl/Shutdown.cpp @@ -19,23 +19,31 @@ #include #include +#include "test_util/TestBase.hpp" + // Anonymous namespace to keep everything file local namespace { -volatile bool did_shutdown = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -struct SimpleMessage {}; +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -class TestReactor : public NUClear::Reactor { +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment), false) { + + on().then([] { // + events.push_back("Shutdown task executed"); + }); - on>().then([this] { - // Shutdown so we can test shutting down + on>, Priority::LOW>().then([this] { + events.push_back("Requesting shutdown"); powerplant.shutdown(); }); - on().then([] { did_shutdown = true; }); + on().then([this] { + events.push_back("Starting test"); + emit(std::make_unique>()); + }); } }; } // namespace @@ -46,10 +54,17 @@ TEST_CASE("A test that a shutdown message is emitted when the system shuts down" config.thread_count = 1; NUClear::PowerPlant plant(config); plant.install(); + plant.start(); - plant.emit(std::make_unique()); + const std::vector expected = { + "Starting test", + "Requesting shutdown", + "Shutdown task executed", + }; - plant.start(); + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); - REQUIRE(did_shutdown); + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/Single.cpp b/tests/dsl/Single.cpp deleted file mode 100644 index 17522e1e0..000000000 --- a/tests/dsl/Single.cpp +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2013 Trent Houliston , Jake Woods - * 2014-2017 Trent Houliston - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the - * Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE - * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR - * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -#include -#include - -namespace { - -struct MessageCount { - MessageCount() = default; - - std::atomic message1{0}; - std::atomic message2{0}; - std::atomic message3{0}; -}; - -MessageCount message_count; // NOLINT(cert-err58-cpp,cppcoreguidelines-avoid-non-const-global-variables) - -struct SimpleMessage1 { - int data{0}; -}; - -struct SimpleMessage2 { - int data{0}; -}; - -struct SimpleMessage3 { - int data{0}; -}; - -class TestReactor : public NUClear::Reactor { -public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { - - on, Single>().then("SimpleMessage1", [this](const SimpleMessage1&) { - // Increment our run count - ++message_count.message1; - - // Emit a message 2 - emit(std::make_unique()); - - // Wait for 10 ms - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - // Emit a message 3 - emit(std::make_unique()); - - // Emit another message 2 - emit(std::make_unique()); - - // We are finished the test - powerplant.shutdown(); - }); - - on, Single>().then("SimpleMessage2", - [](const SimpleMessage2&) { ++message_count.message2; }); - - on, With, Single>().then( - "SimpleMessage2 With SimpleMessage3", - [](const SimpleMessage2&, const SimpleMessage3&) { ++message_count.message3; }); - - on().then("Startup", [this]() { - // Emit two events, only one should run - emit(std::make_unique()); - emit(std::make_unique()); - }); - } -}; -} // namespace - -TEST_CASE("Test that single prevents a second call while one is executing", "[api][precondition][single]") { - - NUClear::PowerPlant::Configuration config; - // Unless there are at least 2 threads here single makes no sense ;) - config.thread_count = 2; - NUClear::PowerPlant plant(config); - plant.install(); - - plant.start(); - - // Require that only 1 run has happened on message 1 - REQUIRE(message_count.message1 == 1); - - // Require that 2 runs have happened on message 2 - REQUIRE(message_count.message2 == 2); - - // Require that only 1 run has happened on message 3 - REQUIRE(message_count.message3 == 1); -} diff --git a/tests/dsl/Startup.cpp b/tests/dsl/Startup.cpp index cd5e158a7..8915c427f 100644 --- a/tests/dsl/Startup.cpp +++ b/tests/dsl/Startup.cpp @@ -19,35 +19,51 @@ #include #include +#include "test_util/TestBase.hpp" + namespace { + +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + struct SimpleMessage { SimpleMessage(int data) : data(data) {} int data{0}; }; -class TestReactor : public NUClear::Reactor { +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { - on>().then([this](const SimpleMessage& message) { - // The message we received should have test == 10 - REQUIRE(message.data == 10); - - // We are finished the test - powerplant.shutdown(); + on>().then([](const SimpleMessage& message) { // + events.push_back("SimpleMessage triggered with " + std::to_string(message.data)); }); - on().then([this]() { emit(std::make_unique(10)); }); + on().then([this]() { + events.push_back("Startup triggered"); + events.push_back("Emitting SimpleMessage"); + emit(std::make_unique(10)); + }); } }; } // namespace TEST_CASE("Testing the startup event is emitted at the start of the program", "[api][startup]") { - NUClear::PowerPlant::Configuration config; config.thread_count = 1; NUClear::PowerPlant plant(config); plant.install(); - plant.start(); + + const std::vector expected = { + "Startup triggered", + "Emitting SimpleMessage", + "SimpleMessage triggered with 10", + }; + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/Sync.cpp b/tests/dsl/Sync.cpp index d9b473c6b..29530e2cb 100644 --- a/tests/dsl/Sync.cpp +++ b/tests/dsl/Sync.cpp @@ -19,103 +19,106 @@ #include #include +#include "test_util/TestBase.hpp" + namespace { +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + template struct Message { - int val; - Message(int val) : val(val){}; + Message(std::string data) : data(std::move(data)){}; + std::string data; }; - -std::atomic semaphore(0); // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -int finished = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -class TestReactor : public NUClear::Reactor { +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment), false) { on>, Sync>().then([this](const Message<0>& m) { - // Increment our semaphore - ++semaphore; + events.push_back("Sync A " + m.data); // Sleep for some time to be safe std::this_thread::sleep_for(std::chrono::milliseconds(5)); - // Check we got the right message - REQUIRE(m.val == 123); - - // Require our semaphore is 1 - REQUIRE(semaphore == 1); - // Emit a message 1 here, it should not run yet - emit(std::make_unique>(10)); + events.push_back("Sync A emitting"); + emit(std::make_unique>("From Sync A")); // Sleep for some time again std::this_thread::sleep_for(std::chrono::milliseconds(5)); - // Decrement our semaphore - --semaphore; + events.push_back("Sync A " + m.data + " finished"); }); on>, Sync>().then([this](const Message<0>& m) { - // Increment our semaphore - ++semaphore; + events.push_back("Sync B " + m.data); // Sleep for some time to be safe std::this_thread::sleep_for(std::chrono::milliseconds(5)); - // Check we got the right message - REQUIRE(m.val == 123); - - // Require our semaphore is 1 - REQUIRE(semaphore == 1); - // Emit a message 1 here, it should not run yet - emit(std::make_unique>(10)); + events.push_back("Sync B emitting"); + emit(std::make_unique>("From Sync B")); // Sleep for some time again std::this_thread::sleep_for(std::chrono::milliseconds(5)); - // Decrement our semaphore - --semaphore; + events.push_back("Sync B " + m.data + " finished"); }); on>, Sync>().then([this](const Message<1>& m) { - // Increment our semaphore - ++semaphore; + events.push_back("Sync C " + m.data); // Sleep for some time to be safe std::this_thread::sleep_for(std::chrono::milliseconds(5)); - // Check we got the right message - REQUIRE(m.val == 10); - - // Require our semaphore is 1 - REQUIRE(semaphore == 1); + // Emit a message 1 here, it should not run yet + events.push_back("Sync C waiting"); // Sleep for some time again std::this_thread::sleep_for(std::chrono::milliseconds(5)); - // Decrement our semaphore - --semaphore; + events.push_back("Sync C " + m.data + " finished"); - if (++finished == 2) { + if (m.data == "From Sync B") { powerplant.shutdown(); } }); - on().then([this] { emit(std::make_unique>(123)); }); + on().then([this] { // + emit(std::make_unique>("From Startup")); + }); } }; } // namespace TEST_CASE("Testing that the Sync word works correctly", "[api][sync]") { - NUClear::PowerPlant::Configuration config; config.thread_count = 4; NUClear::PowerPlant plant(config); plant.install(); - plant.start(); + + const std::vector expected = { + "Sync A From Startup", + "Sync A emitting", + "Sync A From Startup finished", + "Sync B From Startup", + "Sync B emitting", + "Sync B From Startup finished", + "Sync C From Sync A", + "Sync C waiting", + "Sync C From Sync A finished", + "Sync C From Sync B", + "Sync C waiting", + "Sync C From Sync B finished", + }; + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/SingleSync.cpp b/tests/dsl/SyncOrder.cpp similarity index 67% rename from tests/dsl/SingleSync.cpp rename to tests/dsl/SyncOrder.cpp index 696dd37c2..7cd3a3fae 100644 --- a/tests/dsl/SingleSync.cpp +++ b/tests/dsl/SyncOrder.cpp @@ -18,11 +18,16 @@ #include #include +#include #include #include +#include "test_util/TestBase.hpp" + namespace { +constexpr int N_EVENTS = 1000; + struct Message { int val; Message(int val) : val(val){}; @@ -30,39 +35,38 @@ struct Message { struct ShutdownOnIdle {}; -std::vector values; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -class TestReactor : public NUClear::Reactor { +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { - on, Sync>().then("SyncReaction", [](const Message& m) { - values.push_back("Received value " + std::to_string(m.val)); + on, Sync>().then([](const Message& m) { // + events.push_back(m.val); }); - on, Priority::IDLE>().then("ShutdownOnIdle", [this] { powerplant.shutdown(); }); - on().then("Startup", [this] { - values.clear(); - for (int i = 0; i < 1000; ++i) { + for (int i = 0; i < N_EVENTS; ++i) { emit(std::make_unique(i)); } - emit(std::make_unique()); }); } }; } // namespace -TEST_CASE("Testing that the Sync priority queue word works correctly", "[api][sync][priority]") { +TEST_CASE("Sync events execute in order", "[api][sync][priority]") { NUClear::PowerPlant::Configuration config; - config.thread_count = 2; + config.thread_count = 4; NUClear::PowerPlant plant(config); plant.install(); plant.start(); - REQUIRE(values.size() == 1000); - for (int i = 0; i < 1000; ++i) { - CHECK(values[i] == "Received value " + std::to_string(i)); - } + + REQUIRE(events.size() == N_EVENTS); + + std::vector expected(events.size()); + std::iota(expected.begin(), expected.end(), 0); + REQUIRE(events == expected); } diff --git a/tests/dsl/TCP.cpp b/tests/dsl/TCP.cpp index 2830af1f2..36b63b071 100644 --- a/tests/dsl/TCP.cpp +++ b/tests/dsl/TCP.cpp @@ -18,160 +18,236 @@ #include #include -namespace { - -constexpr in_port_t PORT = 40009; -int messages_received = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -// NOLINTNEXTLINE(cert-err58-cpp,cppcoreguidelines-avoid-non-const-global-variables) -const std::string TEST_STRING = "Hello TCP World!"; - -struct Message {}; - -class TestReactor : public NUClear::Reactor { -public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { - // Bind to a known port - on(PORT).then([this](const TCP::Connection& connection) { - on(connection.fd, IO::READ | IO::CLOSE).then([this](IO::Event event) { - // If we read 0 later it means orderly shutdown - ssize_t len = -1; +#include "test_util/TestBase.hpp" +#include "test_util/has_ipv6.hpp" - // We have data to read - if ((event.events & IO::READ) != 0) { - - std::array buff = {0}; - - // Read into the buffer - len = ::recv(event.fd, buff.data(), static_cast(TEST_STRING.size()), 0); - - // 0 indicates orderly shutdown of the socket - if (len != 0) { +namespace { - // Test the data - REQUIRE(len == int(TEST_STRING.size())); - REQUIRE(TEST_STRING == std::string(buff.data())); - ++messages_received; - } - } +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - // The connection was closed and the other test finished - if (len == 0 || ((event.events & IO::CLOSE) != 0) || messages_received == 2) { - if (messages_received == 2) { - known_port_fd.close(); - powerplant.shutdown(); - } - } - }); - }); +enum TestPorts { + KNOWN_V4_PORT = 40010, + KNOWN_V6_PORT = 40011, +}; - // Bind to an unknown port and get the port number - in_port_t bound_port = 0; - std::tie(std::ignore, bound_port, std::ignore) = on().then([this](const TCP::Connection& connection) { - on(connection.fd, IO::READ | IO::CLOSE).then([this](IO::Event event) { - // If we read 0 later it means orderly shutdown - ssize_t len = -1; +enum TestType { + V4_KNOWN, + V4_EPHEMERAL, + V6_KNOWN, + V6_EPHEMERAL, +}; +std::vector active_tests; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - // We have data to read - if ((event.events & IO::READ) != 0) { +in_port_t v4_port = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +in_port_t v6_port = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - std::array buff = {0}; +struct TestConnection { + TestConnection(std::string name, std::string address, in_port_t port) + : name(std::move(name)), address(std::move(address)), port(port) {} + std::string name; + std::string address; + in_port_t port; +}; - // Read into the buffer - len = ::recv(event.fd, buff.data(), static_cast(TEST_STRING.size()), 0); +struct Finished {}; - // 0 indicates orderly shutdown of the socket - if (len != 0) { - // Test the data - REQUIRE(len == int(TEST_STRING.size())); - REQUIRE(TEST_STRING == std::string(buff.data())); - ++messages_received; - } - } +class TestReactor : public test_util::TestBase { +public: + void handle_data(const std::string& name, const IO::Event& event) { + // We have data to read + if ((event.events & IO::READ) != 0) { + + // Read into the buffer + std::array buff{}; + const ssize_t len = ::recv(event.fd, buff.data(), socklen_t(buff.size()), 0); + if (len != 0) { + events.push_back(name + " received: " + std::string(buff.data(), len)); + ::send(event.fd, buff.data(), socklen_t(len), 0); + } + } + + if ((event.events & IO::CLOSE) != 0) { + events.push_back(name + " closed"); + emit(std::make_unique()); + } + } - // The connection was closed and the other test finished - if (len == 0 || ((event.events & IO::CLOSE) != 0) || messages_received == 2) { - if (messages_received == 2) { - bound_port_fd.close(); - powerplant.shutdown(); - } - } - }); - }); + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment), false) { + + // Bind to IPv4 and a known port + for (const auto& t : active_tests) { + switch (t) { + case V4_KNOWN: { + on(KNOWN_V4_PORT).then([this](const TCP::Connection& connection) { + on(connection.fd, IO::READ | IO::CLOSE).then([this](IO::Event event) { + handle_data("v4 Known", event); + }); + }); + } break; + case V4_EPHEMERAL: { + // Bind to IPv4 an unknown port and get the port number + auto v4 = on().then([this](const TCP::Connection& connection) { + on(connection.fd, IO::READ | IO::CLOSE).then([this](IO::Event event) { + handle_data("v4 Ephemeral", event); + }); + }); + v4_port = std::get<1>(v4); + } break; + + // Bind to IPv6 and a known port + case V6_KNOWN: { + on(KNOWN_V6_PORT, "::").then([this](const TCP::Connection& connection) { + on(connection.fd, IO::READ | IO::CLOSE).then([this](IO::Event event) { + handle_data("v6 Known", event); + }); + }); + } break; + + // Bind to IPv6 an unknown port and get the port number + case V6_EPHEMERAL: { + auto v6 = on(0, "::").then([this](const TCP::Connection& connection) { + on(connection.fd, IO::READ | IO::CLOSE).then([this](IO::Event event) { + handle_data("v6 Ephemeral", event); + }); + }); + v6_port = std::get<1>(v6); + } break; + } + } // Send a test message to the known port - on>().then([this] { - // Open a random socket - known_port_fd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); - - // Our address to our local connection - sockaddr_in address{}; - address.sin_family = AF_INET; - address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - address.sin_port = htons(PORT); - - // Connect to ourself - REQUIRE(::connect(known_port_fd, reinterpret_cast(&address), sizeof(address)) == 0); - - // Set linger so we ensure sending all data - linger l{1, 2}; - REQUIRE(::setsockopt(known_port_fd, SOL_SOCKET, SO_LINGER, reinterpret_cast(&l), sizeof(linger)) - == 0); + on, Sync>().then([](const TestConnection& target) { + // Resolve the target address + const NUClear::util::network::sock_t address = NUClear::util::network::resolve(target.address, target.port); - // Write on our socket - const ssize_t sent = - ::send(known_port_fd, TEST_STRING.data(), static_cast(TEST_STRING.size()), 0); - - // We must have sent the right amount of data - REQUIRE(sent == int(TEST_STRING.size())); - }); - - // Send a test message to the freely bound port - on>().then([this, bound_port] { // Open a random socket - bound_port_fd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); - - // Our address to our local connection - sockaddr_in address{}; - address.sin_family = AF_INET; - address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - address.sin_port = htons(bound_port); + NUClear::util::FileDescriptor fd(::socket(address.sock.sa_family, SOCK_STREAM, IPPROTO_TCP), + [](NUClear::fd_t fd) { ::shutdown(fd, SHUT_RDWR); }); + + if (!fd.valid()) { + throw std::runtime_error("Failed to create socket"); + } + + // Set a timeout so we don't hang forever if something goes wrong +#ifdef _WIN32 + DWORD timeout = 500; +#else + timeval timeout{}; + timeout.tv_sec = 0; + timeout.tv_usec = 500000; +#endif + ::setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&timeout), sizeof(timeout)); // Connect to ourself - REQUIRE(::connect(bound_port_fd, reinterpret_cast(&address), sizeof(address)) == 0); - - // Set linger so we ensure sending all data - linger l{1, 2}; - REQUIRE(::setsockopt(bound_port_fd, SOL_SOCKET, SO_LINGER, reinterpret_cast(&l), sizeof(linger)) - == 0); + if (::connect(fd, &address.sock, address.size()) != 0) { + throw std::runtime_error("Failed to connect to socket"); + } // Write on our socket - const ssize_t sent = - ::send(bound_port_fd, TEST_STRING.data(), static_cast(TEST_STRING.size()), 0); + events.push_back(target.name + " sending"); + ::send(fd, target.name.data(), socklen_t(target.name.size()), 0); + + // Receive the echo + std::array buff{}; + const ssize_t recv = ::recv(fd, buff.data(), socklen_t(target.name.size()), 0); + if (recv <= 1) { + events.push_back(target.name + " failed to receive echo"); + } + else { + events.push_back(target.name + " echoed: " + std::string(buff.data(), recv)); + } + }); - // We must have sent the right amount of data - REQUIRE(sent == int(TEST_STRING.size())); + on, Sync>().then([this](const Finished&) { + if (test_no < active_tests.size()) { + switch (active_tests[test_no++]) { + case V4_KNOWN: + emit(std::make_unique("v4 Known", "127.0.0.1", KNOWN_V4_PORT)); + break; + case V4_EPHEMERAL: + emit(std::make_unique("v4 Ephemeral", "127.0.0.1", v4_port)); + break; + case V6_KNOWN: emit(std::make_unique("v6 Known", "::1", KNOWN_V6_PORT)); break; + case V6_EPHEMERAL: emit(std::make_unique("v6 Ephemeral", "::1", v6_port)); break; + default: + events.push_back("Unexpected test"); + powerplant.shutdown(); + break; + } + } + else { + events.push_back("Finishing Test"); + powerplant.shutdown(); + } }); on().then([this] { - // Emit a message just so it will be when everything is running - emit(std::make_unique()); + // Start the first test by emitting a "finished" event + emit(std::make_unique()); }); } private: + size_t test_no = 0; NUClear::util::FileDescriptor known_port_fd; - NUClear::util::FileDescriptor bound_port_fd; + NUClear::util::FileDescriptor ephemeral_port_fd; }; + } // namespace TEST_CASE("Testing listening for TCP connections and receiving data messages", "[api][network][tcp]") { + // First work out what tests will be active + active_tests.push_back(V4_KNOWN); + active_tests.push_back(V4_EPHEMERAL); + if (test_util::has_ipv6()) { + active_tests.push_back(V6_KNOWN); + active_tests.push_back(V6_EPHEMERAL); + } + NUClear::PowerPlant::Configuration config; - config.thread_count = 1; + config.thread_count = 2; NUClear::PowerPlant plant(config); plant.install(); - plant.start(); + + // Get the results for the tests we expect + std::vector expected{}; + for (const auto& t : active_tests) { + switch (t) { + case V4_KNOWN: + expected.push_back("v4 Known sending"); + expected.push_back("v4 Known received: v4 Known"); + expected.push_back("v4 Known echoed: v4 Known"); + expected.push_back("v4 Known closed"); + break; + case V4_EPHEMERAL: + expected.push_back("v4 Ephemeral sending"); + expected.push_back("v4 Ephemeral received: v4 Ephemeral"); + expected.push_back("v4 Ephemeral echoed: v4 Ephemeral"); + expected.push_back("v4 Ephemeral closed"); + break; + case V6_KNOWN: + expected.push_back("v6 Known sending"); + expected.push_back("v6 Known received: v6 Known"); + expected.push_back("v6 Known echoed: v6 Known"); + expected.push_back("v6 Known closed"); + break; + case V6_EPHEMERAL: + expected.push_back("v6 Ephemeral sending"); + expected.push_back("v6 Ephemeral received: v6 Ephemeral"); + expected.push_back("v6 Ephemeral echoed: v6 Ephemeral"); + expected.push_back("v6 Ephemeral closed"); + break; + } + } + expected.push_back("Finishing Test"); + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/Transient.cpp b/tests/dsl/Transient.cpp new file mode 100644 index 000000000..00066a601 --- /dev/null +++ b/tests/dsl/Transient.cpp @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2013 Trent Houliston , Jake Woods + * 2014-2017 Trent Houliston + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include + +#include "test_util/TestBase.hpp" + +namespace { + +struct Message { + Message(std::string msg) : msg(std::move(msg)) {} + std::string msg; +}; + +struct TransientMessage { + TransientMessage(std::string msg = "", bool valid = false) : msg(std::move(msg)), valid(valid) {} + std::string msg; + bool valid; + + operator bool() const { + return valid; + } +}; +} // namespace + +namespace NUClear { +namespace dsl { + namespace trait { + template <> + struct is_transient : public std::true_type {}; + } // namespace trait +} // namespace dsl +} // namespace NUClear + +namespace { + +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +struct TransientGetter : public NUClear::dsl::operation::TypeBind { + + template + static inline TransientMessage get(NUClear::threading::Reaction& r) { + + // Get the real message and return it directly so transient can activate + auto raw = NUClear::dsl::operation::CacheGet::get(r); + if (raw == nullptr) { + return {}; + } + return *raw; + } +}; + +class TestReactor : public test_util::TestBase { +public: + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { + + on, TransientGetter>().then( + [](const Message& m, const TransientMessage& t) { events.push_back(m.msg + " : " + t.msg); }); + + on>, Priority::LOW>().then([this] { + events.push_back("Emitting Message 1"); + emit(std::make_unique("S1")); + }); + on>, Priority::LOW>().then([this] { + events.push_back("Emitting Transient 1"); + emit(std::make_unique("T1", true)); + }); + on>, Priority::LOW>().then([this] { + events.push_back("Emitting Message 2"); + emit(std::make_unique("S2")); + }); + on>, Priority::LOW>().then([this] { + events.push_back("Emitting Invalid Transient 2"); + emit(std::make_unique("T2", false)); + }); + on>, Priority::LOW>().then([this] { + events.push_back("Emitting Message 3"); + emit(std::make_unique("S3")); + }); + on>, Priority::LOW>().then([this] { + events.push_back("Emitting Transient 3"); + emit(std::make_unique("T3", true)); + }); + on>, Priority::LOW>().then([this] { + events.push_back("Emitting Transient 4"); + emit(std::make_unique("T4", true)); + }); + on>, Priority::LOW>().then([this] { + events.push_back("Emitting Invalid Transient 5"); + emit(std::make_unique("T5", false)); + }); + on>, Priority::LOW>().then([this] { + events.push_back("Emitting Message 4"); + emit(std::make_unique("S4")); + }); + + on().then([this] { + emit(std::make_unique>()); + emit(std::make_unique>()); + emit(std::make_unique>()); + emit(std::make_unique>()); + emit(std::make_unique>()); + emit(std::make_unique>()); + emit(std::make_unique>()); + emit(std::make_unique>()); + emit(std::make_unique>()); + }); + } +}; +} // namespace + +TEST_CASE("Testing whether getters that return transient data can cache between calls", "[api][transient]") { + NUClear::PowerPlant::Configuration config; + config.thread_count = 1; + NUClear::PowerPlant plant(config); + plant.install(); + plant.start(); + + const std::vector expected = { + "Emitting Message 1", + "Emitting Transient 1", + "S1 : T1", + "Emitting Message 2", + "S2 : T1", + "Emitting Invalid Transient 2", + "S2 : T1", + "Emitting Message 3", + "S3 : T1", + "Emitting Transient 3", + "S3 : T3", + "Emitting Transient 4", + "S3 : T4", + "Emitting Invalid Transient 5", + "S3 : T4", + "Emitting Message 4", + "S4 : T4", + }; + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); +} diff --git a/tests/dsl/TransientMultiTrigger.cpp b/tests/dsl/TransientMultiTrigger.cpp deleted file mode 100644 index 4c8e3789d..000000000 --- a/tests/dsl/TransientMultiTrigger.cpp +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (C) 2013 Trent Houliston , Jake Woods - * 2014-2017 Trent Houliston - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the - * Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE - * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR - * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -#include -#include - -namespace { - -int value = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -std::vector> value_pairs; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -struct DataType { - int value; - bool good; - - operator bool() const { - return good; - } -}; -} // namespace - -namespace NUClear { -namespace dsl { - namespace trait { - template <> - struct is_transient : public std::true_type {}; - } // namespace trait -} // namespace dsl -} // namespace NUClear - -namespace { -struct SimpleMessage { - SimpleMessage(int value) : value(value){}; - int value; -}; - -struct TransientTypeGetter : public NUClear::dsl::operation::TypeBind { - - template - static inline DataType get(NUClear::threading::Reaction& /*unused*/) { - - // We say for this test that our data is valid if it is odd - return DataType{value, value % 2 == 1}; - } -}; - -class TestReactor : public NUClear::Reactor { -public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { - - on>().then( - [](const DataType& d, const SimpleMessage& m) { value_pairs.push_back(std::make_pair(m.value, d.value)); }); - - on().then([this] { - // Our data starts off as invalid - value = 0; - - // This should not start a run as our data is invalid - emit(std::make_unique(10)); - - // Change our value to 1, our transient data is now valid - value = 1; - - // This should execute our function resulting in the pair 10,1 - emit(std::make_unique(0)); - - // This should make our transient data invalid again - value = 2; - - // This should execute our function resulting in the pair 20,1 - emit(std::make_unique(20)); - - // This should update to a new good value - value = 5; - - // This should execute our function resulting in the pair 30,5 - emit(std::make_unique(30)); - - // This should execute our function resulting in the pair 30,5 - emit(std::make_unique(0)); - - // Value is now bad again - value = 10; - - // This should execute our function resulting in the pair 30,5 - emit(std::make_unique(0)); - // TODO(trent): technically the thing that triggered this resulted in invalid data but used old data, do we - // want to stop this? - // TODO(trent): This would result in two states, invalid data, and non existant data - - // We are finished the test - powerplant.shutdown(); - }); - } -}; -} // namespace - -TEST_CASE("Testing whether getters that return transient data can cache between calls", "[api][transient]") { - - NUClear::PowerPlant::Configuration config; - config.thread_count = 1; - NUClear::PowerPlant plant(config); - plant.install(); - - plant.start(); - - // Now we validate the list (which may be in a different order due to NUClear scheduling) - std::sort(std::begin(value_pairs), std::end(value_pairs)); - - // Check that it was all as expected - REQUIRE(value_pairs.size() == 5); - REQUIRE(value_pairs[0] == std::make_pair(10, 1)); - REQUIRE(value_pairs[1] == std::make_pair(20, 1)); - REQUIRE(value_pairs[2] == std::make_pair(30, 5)); - REQUIRE(value_pairs[3] == std::make_pair(30, 5)); - REQUIRE(value_pairs[4] == std::make_pair(30, 5)); -} diff --git a/tests/dsl/Trigger.cpp b/tests/dsl/Trigger.cpp index 191cee331..ca8eae0fc 100644 --- a/tests/dsl/Trigger.cpp +++ b/tests/dsl/Trigger.cpp @@ -19,37 +19,61 @@ #include #include +#include "test_util/TestBase.hpp" + namespace { + +/// @brief A vector of events that have happened +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + struct SimpleMessage { + SimpleMessage(int data) : data(data) {} int data; }; -class TestReactor : public NUClear::Reactor { +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { - on>().then([this](const SimpleMessage& message) { - // The message we received should have test == 10 - REQUIRE(message.data == 10); + on>().then([](const SimpleMessage& message) { // + events.push_back("Trigger " + std::to_string(message.data)); + }); - // We are finished the test - this->powerplant.shutdown(); + on().then([this] { + // Emit some messages with data + for (int i = 0; i < 10; ++i) { + emit(std::make_unique(i)); + } }); } }; } // namespace -TEST_CASE("A very basic test for Emit and On", "[api][trigger]") { +TEST_CASE("Test that Trigger statements get the correct data", "[api][trigger]") { NUClear::PowerPlant::Configuration config; config.thread_count = 1; NUClear::PowerPlant plant(config); plant.install(); + plant.start(); - auto message = std::make_unique(SimpleMessage{10}); + const std::vector expected = { + "Trigger 0", + "Trigger 1", + "Trigger 2", + "Trigger 3", + "Trigger 4", + "Trigger 5", + "Trigger 6", + "Trigger 7", + "Trigger 8", + "Trigger 9", + }; - plant.emit(message); + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); - plant.start(); + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/UDP.cpp b/tests/dsl/UDP.cpp index ae550b5cb..948c78c06 100644 --- a/tests/dsl/UDP.cpp +++ b/tests/dsl/UDP.cpp @@ -19,77 +19,451 @@ #include #include +#include "test_util/TestBase.hpp" +#include "test_util/has_ipv6.hpp" + namespace { -constexpr uint16_t PORT = 40000; -const std::string TEST_STRING = "Hello UDP World!"; // NOLINT(cert-err58-cpp) -bool received_a = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -bool received_b = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +enum TestPorts { + UNICAST_V4 = 40000, + UNICAST_V6 = 40001, + BROADCAST_V4 = 40002, + MULTICAST_V4 = 40003, + MULTICAST_V6 = 40004, +}; + +const std::string IPV4_MULTICAST_ADDRESS = "230.12.3.22"; // NOLINT(cert-err58-cpp) +const std::string IPV6_MULTICAST_ADDRESS = "ff02::230:12:3:22"; // NOLINT(cert-err58-cpp) + +#ifdef __APPLE__ +// For the IPv6 test we need to bind to the IPv6 localhost address and send from it when using udp emit. +// This is because on OSX without a fully connected IPv6 there is no default route for IPv6 multicast packets +// (see `netstat -nr`) As a result if you don't specify an interface to use when sending and receiving IPv6 multicast +// packets the send/bind fails which makes the tests fail. +const std::string IPV6_BIND = "::1"; // NOLINT(cert-err58-cpp) +#else +const std::string IPV6_BIND = "::"; // NOLINT(cert-err58-cpp) +#endif + +// Ephemeral ports that we will use +in_port_t uni_v4_port = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +in_port_t uni_v6_port = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +in_port_t broad_v4_port = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +in_port_t multi_v4_port = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +in_port_t multi_v6_port = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +enum TestType { + UNICAST_V4_KNOWN, + UNICAST_V4_EPHEMERAL, + UNICAST_V6_KNOWN, + UNICAST_V6_EPHEMERAL, + BROADCAST_V4_KNOWN, + BROADCAST_V4_EPHEMERAL, + MULTICAST_V4_KNOWN, + MULTICAST_V4_EPHEMERAL, + MULTICAST_V6_KNOWN, + MULTICAST_V6_EPHEMERAL, +}; +std::vector active_tests; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +inline std::string get_broadcast_addr() { + static std::string addr{}; + + if (!addr.empty()) { + return addr; + } + + // Get the first IPv4 broadcast address we can find + std::array buff{}; + bool found = false; + for (const auto& iface : NUClear::util::network::get_interfaces()) { + if (iface.ip.sock.sa_family == AF_INET && iface.flags.broadcast) { + ::inet_ntop(AF_INET, &iface.broadcast.ipv4.sin_addr, buff.data(), buff.size()); + found = true; + break; + } + } + if (!found) { + throw std::runtime_error("No broadcast address found"); + } + addr = std::string(buff.data()); + return addr; +} + +struct SendTarget { + std::string data{}; + struct Target { + std::string address{}; + in_port_t port = 0; + }; + Target to{}; + Target from{}; +}; +std::vector send_targets(const std::string& type, in_port_t port) { + std::vector results; + + // Loop through the active tests and add the send targets + // Make sure that the type we are actually after is sent last + for (const auto& t : active_tests) { + switch (t) { + case UNICAST_V4_KNOWN: + case UNICAST_V4_EPHEMERAL: { + results.push_back(SendTarget{type + ":Uv4", {"127.0.0.1", port}, {}}); + } break; + case UNICAST_V6_KNOWN: + case UNICAST_V6_EPHEMERAL: { + results.push_back(SendTarget{type + ":Uv6", {"::1", port}, {}}); + } break; + case BROADCAST_V4_KNOWN: + case BROADCAST_V4_EPHEMERAL: { + results.push_back(SendTarget{type + ":Bv4", {get_broadcast_addr(), port}, {}}); + } break; + case MULTICAST_V4_KNOWN: + case MULTICAST_V4_EPHEMERAL: { + results.push_back(SendTarget{type + ":Mv4", {IPV4_MULTICAST_ADDRESS, port}, {}}); + } break; + case MULTICAST_V6_KNOWN: + case MULTICAST_V6_EPHEMERAL: { + results.push_back(SendTarget{type + ":Mv6", {IPV6_MULTICAST_ADDRESS, port}, {IPV6_BIND, 0}}); + } break; + } + } + + // remove duplicates + results.erase(std::unique(results.begin(), + results.end(), + [](const SendTarget& a, const SendTarget& b) { + return a.to.address == b.to.address && a.to.port == b.to.port && a.data == b.data + && a.from.address == b.from.address && a.from.port == b.from.port; + }), + results.end()); + + // Stable sort so that the type we are after is last + std::stable_sort(results.begin(), results.end(), [](const SendTarget& /*a*/, const SendTarget& b) { + // We want to sort such that the one we are after is last and everything else is unmodified + // That means that every comparision except one should be false + // This is because equality is implied if a < b == false and b < a == false + // The only time we should return true is when b is our target (which would make a less than it) + return b.data.substr(0, 3) == b.data.substr(5, 8); + }); + + return results; +} + +struct Finished { + Finished(std::string name) : name(std::move(name)) {} + std::string name; +}; + +class TestReactor : public test_util::TestBase { +private: + void handle_data(const std::string& name, const UDP::Packet& packet) { + const std::string data(packet.payload.data(), packet.payload.size()); + + // Convert IP address to string in dotted decimal format + const std::string local = packet.local.address + ":" + std::to_string(packet.local.port); -struct Message {}; + events.push_back(name + " <- " + data + " (" + local + ")"); + + if (data == (name + ":" + name.substr(0, 3))) { + emit(std::make_unique(name)); + } + } -class TestReactor : public NUClear::Reactor { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { - - // Known port - on(PORT).then([this](const UDP::Packet& packet) { - // Check that the data we received is correct - REQUIRE(packet.remote.address == "127.0.0.1"); - REQUIRE(packet.payload.size() == TEST_STRING.size()); - REQUIRE(std::memcmp(packet.payload.data(), TEST_STRING.data(), TEST_STRING.size()) == 0); - - received_a = true; - if (received_a && received_b) { - // Shutdown we are done with the test - powerplant.shutdown(); - } - }); + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment), false) { - // Unknown port - in_port_t bound_port = 0; - std::tie(std::ignore, bound_port, std::ignore) = on().then([this](const UDP::Packet& packet) { - // Check that the data we received is correct - REQUIRE(packet.remote.address == "127.0.0.1"); - REQUIRE(packet.payload.size() == TEST_STRING.size()); - REQUIRE(std::memcmp(packet.payload.data(), TEST_STRING.data(), TEST_STRING.size()) == 0); - - received_b = true; - if (received_a && received_b) { - // Shutdown we are done with the test - powerplant.shutdown(); + for (const auto& t : active_tests) { + switch (t) { + case UNICAST_V4_KNOWN: { + on(UNICAST_V4).then([this](const UDP::Packet& packet) { // + handle_data("Uv4K", packet); + }); + } break; + + // IPv4 Unicast Ephemeral port + case UNICAST_V4_EPHEMERAL: { + auto uni_v4 = on().then([this](const UDP::Packet& packet) { // + handle_data("Uv4E", packet); + }); + uni_v4_port = std::get<1>(uni_v4); + } break; + + // IPv6 Unicast Known port + case UNICAST_V6_KNOWN: { + on(UNICAST_V6, "::").then([this](const UDP::Packet& packet) { // + handle_data("Uv6K", packet); + }); + } break; + + // IPv6 Unicast Ephemeral port + case UNICAST_V6_EPHEMERAL: { + auto uni_v6 = on(0, "::").then([this](const UDP::Packet& packet) { // + handle_data("Uv6E", packet); + }); + uni_v6_port = std::get<1>(uni_v6); + } break; + + // IPv4 Broadcast Known port + case BROADCAST_V4_KNOWN: { + on(BROADCAST_V4).then([this](const UDP::Packet& packet) { // + handle_data("Bv4K", packet); + }); + } break; + + // IPv4 Broadcast Ephemeral port + case BROADCAST_V4_EPHEMERAL: { + auto broad_v4 = on().then([this](const UDP::Packet& packet) { // + handle_data("Bv4E", packet); + }); + broad_v4_port = std::get<1>(broad_v4); + } break; + + // No such thing as broadcast in IPv6 + + // IPv4 Multicast Known port + case MULTICAST_V4_KNOWN: { + on(IPV4_MULTICAST_ADDRESS, MULTICAST_V4).then([this](const UDP::Packet& packet) { + handle_data("Mv4K", packet); + }); + } break; + + // IPv4 Multicast Ephemeral port + case MULTICAST_V4_EPHEMERAL: { + auto multi_v4 = on(IPV4_MULTICAST_ADDRESS).then([this](const UDP::Packet& packet) { + handle_data("Mv4E", packet); + }); + multi_v4_port = std::get<1>(multi_v4); + } break; + + // IPv6 Multicast Known port + case MULTICAST_V6_KNOWN: { + on(IPV6_MULTICAST_ADDRESS, MULTICAST_V6, IPV6_BIND) + .then([this](const UDP::Packet& packet) { handle_data("Mv6K", packet); }); + } break; + + // IPv6 Multicast Ephemeral port + case MULTICAST_V6_EPHEMERAL: { + auto multi_v6 = on(IPV6_MULTICAST_ADDRESS, 0, IPV6_BIND) + .then([this](const UDP::Packet& packet) { handle_data("Mv6E", packet); }); + multi_v6_port = std::get<1>(multi_v6); + } break; } - }); + } - // Send a test for a known port - // does not need to include the port in the lambda capture. This is a global variable to the unit test, so - // the function will have access to it. - on>().then([this] { // - emit(std::make_unique(TEST_STRING), "127.0.0.1", PORT); - }); + on>().then([this](const Finished&) { + auto send_all = [this](const std::string& type, const in_port_t& port) { + for (const auto& t : send_targets(type, port)) { + events.push_back(" -> " + t.to.address + ":" + std::to_string(t.to.port)); + try { + emit(std::make_unique(t.data), + t.to.address, + t.to.port, + t.from.address, + t.from.port); + } + catch (std::exception& e) { + events.push_back("Exception: " + std::string(e.what())); + } + } + }; + + if (test_no < active_tests.size()) { + switch (active_tests[test_no++]) { + case UNICAST_V4_KNOWN: { + events.push_back("- Known Unicast V4 Test -"); + send_all("Uv4K", UNICAST_V4); + } break; + + case UNICAST_V4_EPHEMERAL: { + events.push_back("- Ephemeral Unicast V4 Test -"); + send_all("Uv4E", uni_v4_port); + } break; + + case UNICAST_V6_KNOWN: { + events.push_back("- Known Unicast V6 Test -"); + send_all("Uv6K", UNICAST_V6); + } break; + case UNICAST_V6_EPHEMERAL: { + events.push_back("- Ephemeral Unicast V6 Test -"); + send_all("Uv6E", uni_v6_port); + } break; - // Send a test for an unknown port - // needs to include the bound_port in the lambda capture, so that the function will have access to bound_port. - on>().then([this, bound_port] { - // Emit our UDP message - emit(std::make_unique(TEST_STRING), "127.0.0.1", bound_port); + case BROADCAST_V4_KNOWN: { + events.push_back("- Known Broadcast V4 Test -"); + send_all("Bv4K", BROADCAST_V4); + } break; + + case BROADCAST_V4_EPHEMERAL: { + events.push_back("- Ephemeral Broadcast V4 Test -"); + send_all("Bv4E", broad_v4_port); + } break; + + case MULTICAST_V4_KNOWN: { + events.push_back("- Known Multicast V4 Test -"); + send_all("Mv4K", MULTICAST_V4); + } break; + + case MULTICAST_V4_EPHEMERAL: { + events.push_back("- Ephemeral Multicast V4 Test -"); + send_all("Mv4E", multi_v4_port); + } break; + + case MULTICAST_V6_KNOWN: { + events.push_back("- Known Multicast V6 Test -"); + send_all("Mv6K", MULTICAST_V6); + } break; + + case MULTICAST_V6_EPHEMERAL: { + events.push_back("- Ephemeral Multicast V6 Test -"); + send_all("Mv6E", multi_v6_port); + } break; + + default: { + events.push_back("Unknown test type"); + powerplant.shutdown(); + } break; + } + } + else { + powerplant.shutdown(); + } }); on().then([this] { - // Emit a message just so it will be when everything is running - emit(std::make_unique()); + // Start the first test by emitting a "finished" event + emit(std::make_unique("Startup")); }); } + +private: + size_t test_no = 0; }; + } // namespace TEST_CASE("Testing sending and receiving of UDP messages", "[api][network][udp]") { + // Build up the list of active tests based on what we have available + active_tests.push_back(UNICAST_V4_KNOWN); + active_tests.push_back(UNICAST_V4_EPHEMERAL); + if (test_util::has_ipv6()) { + active_tests.push_back(UNICAST_V6_KNOWN); + active_tests.push_back(UNICAST_V6_EPHEMERAL); + } + active_tests.push_back(BROADCAST_V4_KNOWN); + active_tests.push_back(BROADCAST_V4_EPHEMERAL); + active_tests.push_back(MULTICAST_V4_KNOWN); + active_tests.push_back(MULTICAST_V4_EPHEMERAL); + if (test_util::has_ipv6()) { + active_tests.push_back(MULTICAST_V6_KNOWN); + active_tests.push_back(MULTICAST_V6_EPHEMERAL); + } + NUClear::PowerPlant::Configuration config; config.thread_count = 1; NUClear::PowerPlant plant(config); plant.install(); - plant.start(); + + std::vector expected; + for (const auto& t : active_tests) { + switch (t) { + case UNICAST_V4_KNOWN: { + expected.push_back("- Known Unicast V4 Test -"); + for (const auto& line : send_targets("Uv4K", UNICAST_V4)) { + expected.push_back(" -> " + line.to.address + ":" + std::to_string(line.to.port)); + } + expected.push_back("Uv4K <- Uv4K:Uv4 (127.0.0.1:" + std::to_string(UNICAST_V4) + ")"); + } break; + + case UNICAST_V4_EPHEMERAL: { + expected.push_back("- Ephemeral Unicast V4 Test -"); + for (const auto& line : send_targets("Uv4E", uni_v4_port)) { + expected.push_back(" -> " + line.to.address + ":" + std::to_string(line.to.port)); + } + expected.push_back("Uv4E <- Uv4E:Uv4 (127.0.0.1:" + std::to_string(uni_v4_port) + ")"); + } break; + + case UNICAST_V6_KNOWN: { + expected.push_back("- Known Unicast V6 Test -"); + for (const auto& line : send_targets("Uv6K", UNICAST_V6)) { + expected.push_back(" -> " + line.to.address + ":" + std::to_string(line.to.port)); + } + expected.push_back("Uv6K <- Uv6K:Uv6 (::1:" + std::to_string(UNICAST_V6) + ")"); + } break; + + case UNICAST_V6_EPHEMERAL: { + expected.push_back("- Ephemeral Unicast V6 Test -"); + for (const auto& line : send_targets("Uv6E", uni_v6_port)) { + expected.push_back(" -> " + line.to.address + ":" + std::to_string(line.to.port)); + } + expected.push_back("Uv6E <- Uv6E:Uv6 (::1:" + std::to_string(uni_v6_port) + ")"); + } break; + + case BROADCAST_V4_KNOWN: { + expected.push_back("- Known Broadcast V4 Test -"); + for (const auto& line : send_targets("Bv4K", BROADCAST_V4)) { + expected.push_back(" -> " + line.to.address + ":" + std::to_string(line.to.port)); + } + expected.push_back("Bv4K <- Bv4K:Bv4 (" + get_broadcast_addr() + ":" + std::to_string(BROADCAST_V4) + + ")"); + } break; + + case BROADCAST_V4_EPHEMERAL: { + expected.push_back("- Ephemeral Broadcast V4 Test -"); + for (const auto& line : send_targets("Bv4E", broad_v4_port)) { + expected.push_back(" -> " + line.to.address + ":" + std::to_string(line.to.port)); + } + expected.push_back("Bv4E <- Bv4E:Bv4 (" + get_broadcast_addr() + ":" + std::to_string(broad_v4_port) + + ")"); + } break; + + case MULTICAST_V4_KNOWN: { + expected.push_back("- Known Multicast V4 Test -"); + for (const auto& line : send_targets("Mv4K", MULTICAST_V4)) { + expected.push_back(" -> " + line.to.address + ":" + std::to_string(line.to.port)); + } + expected.push_back("Mv4K <- Mv4K:Mv4 (" + IPV4_MULTICAST_ADDRESS + ":" + std::to_string(MULTICAST_V4) + + ")"); + } break; + + case MULTICAST_V4_EPHEMERAL: { + expected.push_back("- Ephemeral Multicast V4 Test -"); + for (const auto& line : send_targets("Mv4E", multi_v4_port)) { + expected.push_back(" -> " + line.to.address + ":" + std::to_string(line.to.port)); + } + expected.push_back("Mv4E <- Mv4E:Mv4 (" + IPV4_MULTICAST_ADDRESS + ":" + std::to_string(multi_v4_port) + + ")"); + } break; + + case MULTICAST_V6_KNOWN: { + expected.push_back("- Known Multicast V6 Test -"); + for (const auto& line : send_targets("Mv6K", MULTICAST_V6)) { + expected.push_back(" -> " + line.to.address + ":" + std::to_string(line.to.port)); + } + expected.push_back("Mv6K <- Mv6K:Mv6 (" + IPV6_MULTICAST_ADDRESS + ":" + std::to_string(MULTICAST_V6) + + ")"); + } break; + + case MULTICAST_V6_EPHEMERAL: { + expected.push_back("- Ephemeral Multicast V6 Test -"); + for (const auto& line : send_targets("Mv6E", multi_v6_port)) { + expected.push_back(" -> " + line.to.address + ":" + std::to_string(line.to.port)); + } + expected.push_back("Mv6E <- Mv6E:Mv6 (" + IPV6_MULTICAST_ADDRESS + ":" + std::to_string(multi_v6_port) + + ")"); + } break; + } + } + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/UDPBroadcast.cpp b/tests/dsl/UDPBroadcast.cpp deleted file mode 100644 index 58888f08b..000000000 --- a/tests/dsl/UDPBroadcast.cpp +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (C) 2013 Trent Houliston , Jake Woods - * 2014-2017 Trent Houliston - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the - * Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE - * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR - * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -#include -#include - -namespace { - -constexpr uint16_t PORT = 40001; -const std::string TEST_STRING = "Hello UDP Broadcast World!"; // NOLINT(cert-err58-cpp) -std::size_t count_a = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -std::size_t count_b = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -std::size_t num_addresses = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -struct Message {}; - -class TestReactor : public NUClear::Reactor { -public: - in_port_t bound_port = 0; - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { - - // Known port - on(PORT).then([this](const UDP::Packet& packet) { - ++count_a; - - // Check that the data we received is correct - REQUIRE(packet.payload.size() == TEST_STRING.size()); - REQUIRE(std::memcmp(packet.payload.data(), TEST_STRING.data(), TEST_STRING.size()) == 0); - - // Shutdown we are done with the test - if (count_a == num_addresses && count_b == num_addresses) { - powerplant.shutdown(); - } - }); - - // Unknown port - std::tie(std::ignore, bound_port, std::ignore) = on().then([this](const UDP::Packet& packet) { - ++count_b; - - // Check that the data we received is correct - REQUIRE(packet.payload.size() == TEST_STRING.size()); - REQUIRE(std::memcmp(packet.payload.data(), TEST_STRING.data(), TEST_STRING.size()) == 0); - - // Shutdown we are done with the test - if (count_a == num_addresses && count_b == num_addresses) { - powerplant.shutdown(); - } - }); - - on>().then([this] { - // Get all the network interfaces - auto interfaces = NUClear::util::network::get_interfaces(); - - std::vector addresses; - for (auto& iface : interfaces) { - // We send on multicast capable addresses - if (iface.broadcast.sock.sa_family == AF_INET && iface.flags.broadcast) { - auto ip_s = iface.broadcast.address(); - if (std::find(std::begin(addresses), std::end(addresses), ip_s.first) == std::end(addresses)) { - addresses.push_back(ip_s.first); - } - } - } - num_addresses = addresses.size(); - - for (auto& ad : addresses) { - - // Send our message to that broadcast address - emit(std::make_unique(TEST_STRING), ad, PORT); - } - }); - - on>().then([this] { - // Get all the network interfaces - auto interfaces = NUClear::util::network::get_interfaces(); - - std::vector addresses; - for (auto& iface : interfaces) { - // We send on multicast capable addresses - if (iface.broadcast.sock.sa_family == AF_INET && iface.flags.broadcast) { - auto ip_s = iface.broadcast.address(); - if (std::find(std::begin(addresses), std::end(addresses), ip_s.first) == std::end(addresses)) { - addresses.push_back(ip_s.first); - } - } - } - num_addresses = addresses.size(); - - for (auto& ad : addresses) { - - // Send our message to that broadcast address - emit(std::make_unique(TEST_STRING), ad, bound_port); - } - }); - - on().then([this] { - // Emit a message just so it will be when everything is running - emit(std::make_unique()); - }); - } -}; -} // namespace - -TEST_CASE("Testing sending and receiving of UDP Broadcast messages", "[api][network][udp][broadcast]") { - - NUClear::PowerPlant::Configuration config; - config.thread_count = 1; - NUClear::PowerPlant plant(config); - plant.install(); - - plant.start(); - - REQUIRE(count_a == num_addresses); - REQUIRE(count_b == num_addresses); -} diff --git a/tests/dsl/UDPMulticastKnownPort.cpp b/tests/dsl/UDPMulticastKnownPort.cpp deleted file mode 100644 index 1ecdb920e..000000000 --- a/tests/dsl/UDPMulticastKnownPort.cpp +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2013 Trent Houliston , Jake Woods - * 2014-2017 Trent Houliston - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the - * Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE - * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR - * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -#include -#include - -namespace { - -constexpr in_port_t PORT = 40002; -const std::string TEST_STRING = "Hello UDP Multicast World!"; // NOLINT(cert-err58-cpp) -const std::string MULTICAST_ADDRESS = "230.12.3.21"; // NOLINT(cert-err58-cpp) -std::size_t count = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -std::size_t num_addresses = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -struct Message {}; - -class TestReactor : public NUClear::Reactor { -public: - bool shutdown_flag = false; - - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { - - // Terminates the test if it takes too long - longer than 200 ms since this reaction first runs - on>().then([this]() { - if (shutdown_flag) { - powerplant.shutdown(); - } - shutdown_flag = true; - }); - - // Known port - on(MULTICAST_ADDRESS, PORT).then([this](const UDP::Packet& packet) { - ++count; - // Check that the data we received is correct - REQUIRE(packet.payload.size() == TEST_STRING.size()); - REQUIRE(std::memcmp(packet.payload.data(), TEST_STRING.data(), TEST_STRING.size()) == 0); - - // Shutdown if we have succeeded - if (count >= num_addresses) { - powerplant.shutdown(); - } - }); - - // Test with known port - on>().then([this] { - // Get all the network interfaces - - // Send our message to that broadcast address - emit(std::make_unique(TEST_STRING), MULTICAST_ADDRESS, PORT); - }); - - on().then([this] { - // Emit a message to start the test - emit(std::make_unique()); - }); - } -}; -} // namespace - -TEST_CASE("Testing sending and receiving of UDP Multicast messages with a known port", - "[api][network][udp][multicast][known_port]") { - - NUClear::PowerPlant::Configuration config; - config.thread_count = 1; - NUClear::PowerPlant plant(config); - plant.install(); - - plant.start(); - - REQUIRE(count == 1); -} diff --git a/tests/dsl/UDPMulticastUnknownPort.cpp b/tests/dsl/UDPMulticastUnknownPort.cpp deleted file mode 100644 index 2c8a6f1a0..000000000 --- a/tests/dsl/UDPMulticastUnknownPort.cpp +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2013 Trent Houliston , Jake Woods - * 2014-2017 Trent Houliston - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the - * Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE - * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR - * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -#include -#include - -namespace { - -const std::string TEST_STRING = "Hello UDP Multicast World!"; // NOLINT(cert-err58-cpp) -const std::string MULTICAST_ADDRESS = "230.12.3.22"; // NOLINT(cert-err58-cpp) -std::size_t count = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -std::size_t num_addresses = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -struct Message {}; - -class TestReactor : public NUClear::Reactor { -public: - bool shutdown_flag = false; - - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { - - // Terminates the test if it takes too long - longer than 200 ms since this reaction first runs - on>().then([this]() { - if (shutdown_flag) { - powerplant.shutdown(); - } - shutdown_flag = true; - }); - - // Unknown port - in_port_t bound_port = 0; - std::tie(std::ignore, bound_port, std::ignore) = - on(MULTICAST_ADDRESS).then([this](const UDP::Packet& packet) { - ++count; - // Check that the data we received is correct - REQUIRE(packet.payload.size() == TEST_STRING.size()); - REQUIRE(std::memcmp(packet.payload.data(), TEST_STRING.data(), TEST_STRING.size()) == 0); - - // Shutdown if we have succeeded - if (count >= num_addresses) { - powerplant.shutdown(); - } - }); - - // Test with port given to us - on>().then([this, bound_port] { - // Get all the network interfaces - // Send our message to that broadcast address - emit(std::make_unique(TEST_STRING), MULTICAST_ADDRESS, bound_port); - }); - - on().then([this] { - // Emit a message to start the test - emit(std::make_unique()); - }); - } -}; -} // namespace - -TEST_CASE("Testing sending and receiving of UDP Multicast messages with an unknown port", - "[api][network][udp][multicast][unknown_port]") { - - NUClear::PowerPlant::Configuration config; - config.thread_count = 1; - NUClear::PowerPlant plant(config); - plant.install(); - - plant.start(); - - REQUIRE(count == 1); -} diff --git a/tests/dsl/Watchdog.cpp b/tests/dsl/Watchdog.cpp index e82e1c0b4..1d3981a34 100644 --- a/tests/dsl/Watchdog.cpp +++ b/tests/dsl/Watchdog.cpp @@ -21,97 +21,75 @@ #include #include +#include "test_util/TestBase.hpp" + namespace { -NUClear::clock::time_point start; // NOLINT(cert-err58-cpp,cppcoreguidelines-avoid-non-const-global-variables) -NUClear::clock::time_point end; // NOLINT(cert-err58-cpp,cppcoreguidelines-avoid-non-const-global-variables) -NUClear::clock::time_point end_a; // NOLINT(cert-err58-cpp,cppcoreguidelines-avoid-non-const-global-variables) -NUClear::clock::time_point end_b; // NOLINT(cert-err58-cpp,cppcoreguidelines-avoid-non-const-global-variables) -bool a_ended = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -bool b_ended = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -int count = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -#ifdef _WIN32 -// The precision of timing on Windows (with the current NUClear timing method) is not great. -// This defines the intervals larger to avoid the problems at smaller intervals -// TODO(Josephus or Trent): use a higher precision timing method on Windows (look into nanosleep, -// in addition to the condition lock and spin lock used in ChronoController.hpp) -constexpr int WATCHDOG_TIMEOUT = 30; -constexpr int EVERY_INTERVAL = 5; -#else -constexpr int WATCHDOG_TIMEOUT = 10; -constexpr int EVERY_INTERVAL = 5; -#endif - -class TestReactor : public NUClear::Reactor { -public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - start = NUClear::clock::now(); - count = 0; +template +struct Flag {}; - // Trigger the watchdog after WATCHDOG_TIMEOUT milliseconds - on>().then([this] { - end = NUClear::clock::now(); +class TestReactor : public test_util::TestBase { +public: + TestReactor(std::unique_ptr environment) + : TestBase(std::move(environment), false), start(NUClear::clock::now()) { - // When our watchdog eventually triggers, shutdown + on, 50, std::chrono::milliseconds>>().then([this] { + events.push_back("Watchdog 1 triggered @ " + floored_time()); powerplant.shutdown(); }); - // Service the watchdog every EVERY_INTERVAL milliseconds, 20 times. Then let it expire to trigger and end the - // test. - on>().then([this] { - // service the watchdog - if (++count < 20) { - emit(ServiceWatchdog()); + on, 40, std::chrono::milliseconds>>().then([this] { + if (flag2++ < 3) { + events.push_back("Watchdog 2 triggered @ " + floored_time()); + emit(ServiceWatchdog>()); } }); - } -}; -class TestReactorRuntimeArg : public NUClear::Reactor { -public: - TestReactorRuntimeArg(std::unique_ptr environment) : Reactor(std::move(environment)) { - - start = NUClear::clock::now(); - count = 0; - - // Trigger the watchdog after WATCHDOG_TIMEOUT milliseconds - on>(std::string("test a")) - .then([this] { - end_a = NUClear::clock::now(); - a_ended = true; - - // When our watchdog eventually triggers, shutdown - if (b_ended) { - powerplant.shutdown(); - } - }); - - // Trigger the watchdog after WATCHDOG_TIMEOUT milliseconds - on>(std::string("test b")) - .then([this] { - end_b = NUClear::clock::now(); - b_ended = true; - - // When our watchdog eventually triggers, shutdown - if (a_ended) { - powerplant.shutdown(); - } - }); - - // Service the watchdog every EVERY_INTERVAL milliseconds, 20 times. Then let it expire to trigger and end the - // test. - on>().then([this] { - // service the watchdog - if (++count < 20) { - emit(ServiceWatchdog(std::string("test a"))); - emit(ServiceWatchdog(std::string("test b"))); + // Watchdog with subtypes + on, 30, std::chrono::milliseconds>>('a').then([this] { + if (flag3a++ < 3) { + events.push_back("Watchdog 3A triggered @ " + floored_time()); + emit(ServiceWatchdog>()); + emit(ServiceWatchdog>()); } }); + on, 20, std::chrono::milliseconds>>('b').then([this] { + if (flag3b++ < 3) { + events.push_back("Watchdog 3B triggered @ " + floored_time()); + emit(ServiceWatchdog>()); + emit(ServiceWatchdog>()); + emit(ServiceWatchdog>('a')); + } + }); + + on, 10, std::chrono::milliseconds>>().then([this] { + if (flag4++ < 3) { + events.push_back("Watchdog 4 triggered @ " + floored_time()); + emit(ServiceWatchdog>()); + emit(ServiceWatchdog>()); + emit(ServiceWatchdog>('a')); + emit(ServiceWatchdog>('b')); + } + }); + } + + std::string floored_time() const { + using namespace std::chrono; // NOLINT(google-build-using-namespace) fine in function scope + const double diff = duration_cast>(NUClear::clock::now() - start).count(); + // Round to 100ths of a second + return std::to_string(int(std::floor(diff * 100))); } + + NUClear::clock::time_point start; + int flag2{0}; + int flag3a{0}; + int flag3b{0}; + int flag4{0}; }; + } // namespace TEST_CASE("Testing the Watchdog Smart Type", "[api][watchdog]") { @@ -120,23 +98,27 @@ TEST_CASE("Testing the Watchdog Smart Type", "[api][watchdog]") { config.thread_count = 1; NUClear::PowerPlant plant(config); plant.install(); - - plant.start(); - - // Require that at least the minimum time interval to have run all Everys has passed - REQUIRE(end - start > std::chrono::milliseconds(20 * EVERY_INTERVAL)); -} - -TEST_CASE("Testing the Watchdog Smart Type with a sub type", "[api][watchdog][sub_type]") { - - NUClear::PowerPlant::Configuration config; - config.thread_count = 1; - NUClear::PowerPlant plant(config); - plant.install(); - plant.start(); - // Require that at least the minimum time interval to have run all Everys has passed - REQUIRE(end_a - start > std::chrono::milliseconds(20 * EVERY_INTERVAL)); - REQUIRE(end_b - start > std::chrono::milliseconds(20 * EVERY_INTERVAL)); + const std::vector expected = { + "Watchdog 4 triggered @ 1", + "Watchdog 4 triggered @ 2", + "Watchdog 4 triggered @ 3", + "Watchdog 3B triggered @ 5", + "Watchdog 3B triggered @ 7", + "Watchdog 3B triggered @ 9", + "Watchdog 3A triggered @ 12", + "Watchdog 3A triggered @ 15", + "Watchdog 3A triggered @ 18", + "Watchdog 2 triggered @ 22", + "Watchdog 2 triggered @ 26", + "Watchdog 2 triggered @ 30", + "Watchdog 1 triggered @ 35", + }; + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/With.cpp b/tests/dsl/With.cpp index d51b4077f..aaee9c166 100644 --- a/tests/dsl/With.cpp +++ b/tests/dsl/With.cpp @@ -19,45 +19,87 @@ #include #include +#include "test_util/TestBase.hpp" + namespace { -struct DifferentOrderingMessage1 { - int a; -}; -struct DifferentOrderingMessage2 { - int a; +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +struct Message { + Message(std::string data) : data(std::move(data)) {} + std::string data; }; -struct DifferentOrderingMessage3 { - int a; +struct Data { + Data(std::string data) : data(std::move(data)) {} + std::string data; }; -class DifferentOrderingReactor : public NUClear::Reactor { +class TestReactor : public test_util::TestBase { public: - DifferentOrderingReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { // Check that the lists are combined, and that the function args are in order - on, Trigger, With>().then( - [this](const DifferentOrderingMessage1&, - const DifferentOrderingMessage3&, - const DifferentOrderingMessage2&) { this->powerplant.shutdown(); }); - - // Make sure we can pass an empty function in here - on, With>().then( - [] {}); + on, With>().then([](const Message& m, const Data& d) { // + events.push_back("Message: " + m.data + " Data: " + d.data); + }); + + on>, Priority::LOW>().then([this] { + events.push_back("Emitting Data 1"); + emit(std::make_unique("D1")); + }); + + on>, Priority::LOW>().then([this] { + events.push_back("Emitting Data 2"); + emit(std::make_unique("D2")); + }); + + on>, Priority::LOW>().then([this] { + events.push_back("Emitting Message 1"); + emit(std::make_unique("M1")); + }); + + on>, Priority::LOW>().then([this] { + events.push_back("Emitting Data 3"); + emit(std::make_unique("D3")); + }); + + on>, Priority::LOW>().then([this] { + events.push_back("Emitting Message 2"); + emit(std::make_unique("M2")); + }); + + on().then([this] { + emit(std::make_unique>()); + emit(std::make_unique>()); + emit(std::make_unique>()); + emit(std::make_unique>()); + emit(std::make_unique>()); + }); } }; } // namespace -TEST_CASE("Testing poorly ordered on arguments", "[api][with]") { +TEST_CASE("Testing the with dsl keyword", "[api][with]") { NUClear::PowerPlant::Configuration config; config.thread_count = 1; NUClear::PowerPlant plant(config); - plant.install(); + plant.install(); + plant.start(); + + const std::vector expected = { + "Emitting Data 1", + "Emitting Data 2", + "Emitting Message 1", + "Message: M1 Data: D2", + "Emitting Data 3", + "Emitting Message 2", + "Message: M2 Data: D3", + }; - plant.emit(std::make_unique()); - plant.emit(std::make_unique()); - plant.emit(std::make_unique()); + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); - REQUIRE_NOTHROW(plant.start()); - REQUIRE_FALSE(plant.running()); + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/emit/Delay.cpp b/tests/dsl/emit/Delay.cpp index f3cba12c0..b38fbcd2e 100644 --- a/tests/dsl/emit/Delay.cpp +++ b/tests/dsl/emit/Delay.cpp @@ -19,49 +19,79 @@ #include #include +#include "../../test_util/TestBase.hpp" + // Anonymous namespace to keep everything file local namespace { -struct DelayMessage {}; -struct AtTimeMessage {}; -struct NormalMessage {}; +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +/// @brief Test units are the time units the test is performed in +using TestUnits = std::chrono::duration>; +/// @brief Perform this many different time points for the test +constexpr int test_loops = 5; + +struct DelayedMessage { + DelayedMessage(const NUClear::clock::duration& delay) : time(NUClear::clock::now()), delay(delay) {} + NUClear::clock::time_point time; + NUClear::clock::duration delay; +}; + +struct TargetTimeMessage { + TargetTimeMessage(const NUClear::clock::time_point& target) : time(NUClear::clock::now()), target(target) {} + NUClear::clock::time_point time; + NUClear::clock::time_point target; +}; -// NOLINTNEXTLINE(cert-err58-cpp,cppcoreguidelines-avoid-non-const-global-variables) -NUClear::clock::time_point sent; -// NOLINTNEXTLINE(cert-err58-cpp,cppcoreguidelines-avoid-non-const-global-variables) -NUClear::clock::time_point normal_received; -// NOLINTNEXTLINE(cert-err58-cpp,cppcoreguidelines-avoid-non-const-global-variables) -NUClear::clock::time_point delay_received; -// NOLINTNEXTLINE(cert-err58-cpp,cppcoreguidelines-avoid-non-const-global-variables) -NUClear::clock::time_point at_time_received; +struct FinishTest {}; -class TestReactor : public NUClear::Reactor { +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { - emit(std::make_unique(5)); + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment), false) { - // This message should come in later - on>().then([this] { - delay_received = NUClear::clock::now(); + // Measure when messages were sent and received and print those values + on>().then([](const DelayedMessage& m) { + auto true_delta = std::chrono::duration_cast(NUClear::clock::now() - m.time); + auto delta = std::chrono::duration_cast(m.delay); - powerplant.shutdown(); + // Print the debug message + events.push_back("delayed " + std::to_string(true_delta.count()) + " received " + + std::to_string(delta.count())); }); - on>().then([] { - // Don't shut down here we are first - at_time_received = NUClear::clock::now(); + on>().then([](const TargetTimeMessage& m) { + auto true_delta = std::chrono::duration_cast(NUClear::clock::now() - m.time); + auto delta = std::chrono::duration_cast(m.target - m.time); + + // Print the debug message + events.push_back("at_time " + std::to_string(true_delta.count()) + " received " + + std::to_string(delta.count())); + }); + + on>().then([this] { + events.push_back("Finished"); + powerplant.shutdown(); }); - on>().then([] { normal_received = NUClear::clock::now(); }); on().then([this] { - sent = NUClear::clock::now(); - emit(std::make_unique()); + // Get our jump size in milliseconds + const int jump_unit = (TestUnits::period::num * 1000) / TestUnits::period::den; + // Delay with consistent jumps + for (int i = 0; i < test_loops; ++i) { + auto delay = std::chrono::milliseconds(jump_unit * i); + emit(std::make_unique(delay), delay); + } + + // Target time with consistent jumps that interleave the first set + for (int i = 0; i < test_loops; ++i) { + auto target = NUClear::clock::now() + std::chrono::milliseconds(jump_unit / 2 + jump_unit * i); + emit(std::make_unique(target), target); + } - // Delay by 200, and a message 100ms in the future, the 200ms one should come in first - emit(std::make_unique(), std::chrono::milliseconds(200)); - emit(std::make_unique(), - NUClear::clock::now() + std::chrono::milliseconds(100)); + // Emit a shutdown one time unit after + emit(std::make_unique(), std::chrono::milliseconds(jump_unit * (test_loops + 1))); }); } }; @@ -72,12 +102,25 @@ TEST_CASE("Testing the delay emit", "[api][emit][delay]") { config.thread_count = 1; NUClear::PowerPlant plant(config); plant.install(); - plant.start(); - // Ensure the message delays are correct, I would make these bounds tighter, but travis is pretty dumb - REQUIRE(std::chrono::duration_cast(delay_received - sent).count() > 190); - REQUIRE(std::chrono::duration_cast(delay_received - sent).count() < 225); - REQUIRE(std::chrono::duration_cast(at_time_received - sent).count() > 90); - REQUIRE(std::chrono::duration_cast(at_time_received - sent).count() < 120); + const std::vector expected = { + "delayed 0 received 0", + "at_time 0 received 0", + "delayed 1 received 1", + "at_time 1 received 1", + "delayed 2 received 2", + "at_time 2 received 2", + "delayed 3 received 3", + "at_time 3 received 3", + "delayed 4 received 4", + "at_time 4 received 4", + "Finished", + }; + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/emit/EmitFusion.cpp b/tests/dsl/emit/EmitFusion.cpp index 8af9ee1ed..fe2c41fa4 100644 --- a/tests/dsl/emit/EmitFusion.cpp +++ b/tests/dsl/emit/EmitFusion.cpp @@ -20,108 +20,86 @@ #include #include +#include "../../test_util/TestBase.hpp" + // Anonymous namespace to keep everything file local namespace { -int v1 = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -int v2 = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -int v3 = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -int stored_a = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -double stored_c = 0.0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -double stored_d = 0.0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -std::string stored_b; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) template -struct EmitTester1 { - static inline void emit(NUClear::PowerPlant& /*unused*/, std::shared_ptr p, int a, std::string b) { - v1 = *p; - stored_a = a; - stored_b = std::move(b); +struct E1 { + static inline void emit(NUClear::PowerPlant& /*unused*/, std::shared_ptr p, const int& a, const std::string& b) { + events.push_back("E1a " + *p + " " + std::to_string(a) + " " + b); } - static inline void emit(NUClear::PowerPlant& /*unused*/, std::shared_ptr p, double c) { - v2 = *p; - stored_c = c; + static inline void emit(NUClear::PowerPlant& /*unused*/, std::shared_ptr p, const std::string& c) { + events.push_back("E1b " + *p + " " + c); } }; template -struct EmitTester2 { - static inline void emit(NUClear::PowerPlant& /*unused*/, std::shared_ptr p, double d) { - v3 = *p; - stored_d = d; +struct E2 { + static inline void emit(NUClear::PowerPlant& /*unused*/, std::shared_ptr p, const bool& d) { + events.push_back("E2a " + *p + " " + (d ? "true" : "false")); + } + + static inline void emit(NUClear::PowerPlant& /*unused*/, std::shared_ptr p, const int& e, const std::string& f) { + events.push_back("E2b " + *p + " " + std::to_string(e) + " " + f); } }; -class TestReactor : public NUClear::Reactor { +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { - - // Make some things to emit - auto t1 = std::make_unique(8); - auto t2 = std::make_unique(10); - auto t3 = std::make_unique(52); - auto t4 = std::make_unique(100); - - // Test using the second overload - emit(t1, 7.2); - REQUIRE(v1 == 0); - REQUIRE(v2 == 8); - v2 = 0; - REQUIRE(v3 == 0); - REQUIRE(stored_a == 0); - REQUIRE(stored_b.empty()); - REQUIRE(stored_c == 7.2); - stored_c = 0; - REQUIRE(stored_d == 0); - - // Test using the first overload - emit(t2, 1337, "This is text"); - REQUIRE(v1 == 10); - v1 = 0; - REQUIRE(v2 == 0); - REQUIRE(v3 == 0); - REQUIRE(stored_a == 1337); - stored_a = 0; - REQUIRE(stored_b == "This is text"); - stored_b = ""; - REQUIRE(stored_c == 0); - REQUIRE(stored_d == 0); - - // Test multiple functions - emit(t3, 15, 8.3); - REQUIRE(v1 == 0); - REQUIRE(v2 == 52); - v2 = 0; - REQUIRE(v3 == 52); - v3 = 0; - REQUIRE(stored_a == 0); - REQUIRE(stored_b.empty()); - REQUIRE(stored_c == 15); - stored_c = 0; - REQUIRE(stored_d == 8.3); - stored_d = 0; - - // Test even more multiple functions - emit(t4, 2, "Hello World", 9.2, 5); - REQUIRE(v1 == 100); - REQUIRE(v2 == 100); - REQUIRE(v3 == 100); - REQUIRE(stored_a == 2); - REQUIRE(stored_b == "Hello World"); - REQUIRE(stored_c == 5); - REQUIRE(stored_d == 9.2); - - on().then([this] { powerplant.shutdown(); }); + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { + + // Emit some messages + emit(std::make_unique("message1"), "test1"); // 1b + events.push_back("End test 1"); + + emit(std::make_unique("message2"), 1337, "test2"); // 1a + events.push_back("End test 2"); + + emit(std::make_unique("message3"), 15, "test3", true); // 1a, 2a + events.push_back("End test 3"); + + emit(std::make_unique("message4"), 2, "Hello World", false, "test4"); // 1a, 2a, 1b + events.push_back("End test 4"); + + emit(std::make_unique("message5"), 5, "test5a", 10, "test5b"); // 1a, 2b + events.push_back("End test 5"); } }; } // namespace TEST_CASE("Testing emit function fusion", "[api][emit][fusion]") { + NUClear::PowerPlant::Configuration config; config.thread_count = 1; NUClear::PowerPlant plant(config); plant.install(); - plant.start(); + const std::vector expected = { + "E1b message1 test1", + "End test 1", + "E1a message2 1337 test2", + "End test 2", + "E1a message3 15 test3", + "E2a message3 true", + "End test 3", + "E1a message4 2 Hello World", + "E2a message4 false", + "E1b message4 test4", + "End test 4", + "E1a message5 5 test5a", + "E2b message5 10 test5b", + "End test 5", + }; + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/emit/Initialise.cpp b/tests/dsl/emit/Initialise.cpp index ffe9333a8..4c11744f4 100644 --- a/tests/dsl/emit/Initialise.cpp +++ b/tests/dsl/emit/Initialise.cpp @@ -19,27 +19,42 @@ #include #include +#include "../../test_util/TestBase.hpp" + // Anonymous namespace to keep everything file local namespace { -struct ShutdownNowPlx {}; +/// @brief Events that occur during the test +std::vector events; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +struct TestMessage { + TestMessage(std::string data) : data(std::move(data)) {} + std::string data; +}; -class TestReactor : public NUClear::Reactor { +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { - emit(std::make_unique(5)); + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { + emit(std::make_unique("Initialise before trigger")); + emit(std::make_unique("Normal before trigger")); - on>().then([this](const int& v) { - REQUIRE(v == 5); + on>().then([](const TestMessage& v) { // + events.push_back("Triggered " + v.data); + }); + + emit(std::make_unique("Normal after trigger")); - // We can't call shutdown here because - // we haven't started yet. That's because - // emits from Scope::INITIALIZE are not - // considered fully "initialized" - emit(std::make_unique()); + on>>().then([this] { // + emit(std::make_unique("Initialise post startup")); + }); + on>>().then([this] { // + emit(std::make_unique("Normal post startup")); }); - on>().then([this] { powerplant.shutdown(); }); + on().then([this] { + emit(std::make_unique>()); + emit(std::make_unique>()); + }); } }; } // namespace @@ -49,6 +64,18 @@ TEST_CASE("Testing the Initialize scope", "[api][emit][initialize]") { config.thread_count = 1; NUClear::PowerPlant plant(config); plant.install(); - plant.start(); + + const std::vector expected = { + "Triggered Normal after trigger", + "Triggered Initialise before trigger", + "Triggered Initialise post startup", + "Triggered Normal post startup", + }; + + // Make an info print the diff in an easy to read way if we fail + INFO(test_util::diff_string(expected, events)); + + // Check the events fired in order and only those events + REQUIRE(events == expected); } diff --git a/tests/dsl/emit/UDP.cpp b/tests/dsl/emit/UDP.cpp deleted file mode 100644 index 84ffb3e5d..000000000 --- a/tests/dsl/emit/UDP.cpp +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2013 Trent Houliston , Jake Woods - * 2014-2017 Trent Houliston - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the - * Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE - * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR - * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -#include -#include - -// Anonymous namespace to keep everything file local -namespace { - -int received_messages = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -class TestReactor : public NUClear::Reactor { -public: - in_port_t bound_port = 0; - - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { - emit(std::make_unique(5)); - - std::tie(std::ignore, bound_port, std::ignore) = on().then([this](const UDP::Packet& packet) { - ++received_messages; - - switch (packet.payload.front()) { - case 'a': - case 'b': - REQUIRE(packet.remote.address == "127.0.0.1"); - REQUIRE(packet.local.address == "127.0.0.1"); - REQUIRE(packet.local.port == bound_port); - break; - case 'c': - REQUIRE(packet.remote.address == "127.0.0.1"); - REQUIRE(packet.remote.port == 12345); - REQUIRE(packet.local.address == "127.0.0.1"); - REQUIRE(packet.local.port == bound_port); - break; - case 'd': - REQUIRE(packet.remote.address == "127.0.0.1"); - REQUIRE(packet.remote.port == 54321); - REQUIRE(packet.local.address == "127.0.0.1"); - REQUIRE(packet.local.port == bound_port); - break; - } - - if (received_messages == 4) { - powerplant.shutdown(); - } - }); - - on().then([this] { - // Send using a string - emit(std::make_unique('a'), "127.0.0.1", bound_port); - emit(std::make_unique('b'), "127.0.0.1", bound_port); - emit(std::make_unique('c'), "127.0.0.1", bound_port, "", in_port_t(12345)); - emit(std::make_unique('d'), "127.0.0.1", bound_port, "", in_port_t(54321)); - }); - } -}; -} // namespace - -TEST_CASE("Testing UDP emits work correctly", "[api][emit][udp]") { - NUClear::PowerPlant::Configuration config; - config.thread_count = 1; - NUClear::PowerPlant plant(config); - plant.install(); - - plant.start(); -} diff --git a/tests/individual/BaseClock.cpp b/tests/individual/BaseClock.cpp index eeca152eb..cc707b7ce 100644 --- a/tests/individual/BaseClock.cpp +++ b/tests/individual/BaseClock.cpp @@ -28,6 +28,7 @@ #include #include "message/ReactionStatistics.hpp" +#include "test_util/TestBase.hpp" // Anonymous namespace to keep everything file local namespace { @@ -51,7 +52,7 @@ class TestReactor : public NUClear::Reactor { on>().then( [this](const NUClear::message::ReactionStatistics& stats) { const std::lock_guard lock(times_mutex); - times.push_back(std::make_pair(stats.emitted, std::chrono::system_clock::now())); + times.push_back(std::make_pair(stats.finished, std::chrono::system_clock::now())); if (times.size() > n_time) { powerplant.shutdown(); } diff --git a/tests/individual/CustomClock.cpp b/tests/individual/CustomClock.cpp index 17aab7369..c4bfcf766 100644 --- a/tests/individual/CustomClock.cpp +++ b/tests/individual/CustomClock.cpp @@ -23,6 +23,8 @@ #define NUCLEAR_CUSTOM_CLOCK #include +#include "test_util/TestBase.hpp" + // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now(); @@ -42,19 +44,20 @@ template struct Message {}; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -std::vector times; -constexpr int n_time = 100; +std::vector> times; -class TestReactor : public NUClear::Reactor { +class TestReactor : public test_util::TestBase { public: - TestReactor(std::unique_ptr environment) : Reactor(std::move(environment)) { - - // Running every this slowed down clock should execute slower - on>().then([this] { - times.push_back(std::chrono::steady_clock::now()); - if (times.size() > n_time) { - powerplant.shutdown(); - } + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment), false) { + + // Collect steady clock times as well as NUClear clock times + on>().then([] { // + times.emplace_back(std::chrono::steady_clock::now(), NUClear::clock::now()); + }); + + // Collect until the watchdog times out + on>().then([this] { // + powerplant.shutdown(); }); } }; @@ -65,24 +68,22 @@ TEST_CASE("Testing custom clock works correctly", "[api][custom_clock]") { NUClear::PowerPlant::Configuration config; config.thread_count = 1; NUClear::PowerPlant plant(config); - - // We are installing with an initial log level of debug plant.install(); - plant.start(); - // Build up our difference vector - double total = 0.0; - for (size_t i = 0; i < times.size() - 1; ++i) { - total += (double((times[i + 1] - times[i]).count()) / double(std::nano::den)); + // Calculate the average ratio delta time for steady and custom clocks + double steady_total = 0; + double custom_total = 0; + + for (int i = 0; i + 1 < int(times.size()); ++i) { + using namespace std::chrono; // NOLINT(google-build-using-namespace) fine in function scope + steady_total += duration_cast>(times[i + 1].first - times[i].first).count(); + custom_total += duration_cast>(times[i + 1].second - times[i].second).count(); } -#ifdef _WIN32 - const double timing_epsilon = 1e-2; -#else - const double timing_epsilon = 1e-3; -#endif + // The ratio should be about 0.5 + REQUIRE((custom_total / steady_total) == Approx(0.5)); - // The total should be about 2.0 - REQUIRE(total == Approx(2.0).epsilon(timing_epsilon)); + // The amount of time that passed should be (n - 1) * 2 * 10ms + REQUIRE(steady_total == Approx(2.0 * (times.size() - 1) * 1e-2).margin(1e-3)); } diff --git a/tests/test_util/TestBase.hpp b/tests/test_util/TestBase.hpp new file mode 100644 index 000000000..00630e65e --- /dev/null +++ b/tests/test_util/TestBase.hpp @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2013 Trent Houliston , Jake Woods + * 2014-2023 Trent Houliston + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef TEST_UTIL_TESTBASE_HPP +#define TEST_UTIL_TESTBASE_HPP + +#include +#include +#include + +#include "nuclear" +#include "test_util/diff_string.hpp" + +namespace test_util { + +template +class TestBase : public NUClear::Reactor { +public: + /** + * @brief Struct to use to emit each step of the test, by doing each step in a separate reaction with low priority, + * it will ensure that everything has finished changing before the next step is run + * + * @tparam i the number of the step + */ + template + struct Step {}; + + /** + * @brief Struct to handle shutting down the powerplant when the system is idle (i.e. the unit test(s) are finished) + */ + struct ShutdownOnIdle {}; + + explicit TestBase(std::unique_ptr environment, const bool& shutdown_on_idle = true) + : Reactor(std::move(environment)) { + + // Shutdown if the system is idle + on, Priority::IDLE>().then([this] { powerplant.shutdown(); }); + on().then([this, shutdown_on_idle] { + if (shutdown_on_idle) { + emit(std::make_unique()); + } + }); + + // Timeout if the test doesn't complete in time + on, MainThread>().then([this] { + powerplant.shutdown(); + INFO("Test timed out"); + CHECK(false); + }); + } +}; + +} // namespace test_util + +#endif // TEST_UTIL_TESTBASE_HPP diff --git a/tests/test_util/diff_string.cpp b/tests/test_util/diff_string.cpp new file mode 100644 index 000000000..0eb8a8a0d --- /dev/null +++ b/tests/test_util/diff_string.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2013 Trent Houliston , Jake Woods + * 2014-2023 Trent Houliston + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include "diff_string.hpp" + +#include + +#include "lcs.hpp" + +namespace test_util { + +std::string diff_string(const std::vector& expected, const std::vector& actual) { + + // Find the longest string in each side or "Expected" and "Actual" if those are the longest + auto len = [](const std::string& a, const std::string& b) { return a.size() < b.size(); }; + auto max_a_it = std::max_element(expected.begin(), expected.end(), len); + auto max_b_it = std::max_element(actual.begin(), actual.end(), len); + const int max_a = std::max(int(std::strlen("Expected")), max_a_it != expected.end() ? int(max_a_it->size()) : 0); + const int max_b = std::max(int(std::strlen("Actual")), max_b_it != actual.end() ? int(max_b_it->size()) : 0); + + // Start with a header + std::string output = std::string("Expected") + std::string(max_a - 8, ' ') + " | " + std::string("Actual") + + std::string(max_b - 6, ' ') + "\n"; + + // Print a divider characters for a divider + output += std::string(9 + max_a + max_b, '-') + "\n"; + + auto matches = lcs(expected, actual); + auto& match_a = matches.first; + auto& match_b = matches.second; + int i_a = 0; + int i_b = 0; + while (i_a < int(expected.size()) && i_b < int(actual.size())) { + if (match_a[i_a]) { + if (match_b[i_b]) { + output += expected[i_a] + std::string(max_a - int(expected[i_a].size()), ' ') + " <-> " + + actual[i_b] + std::string(max_b - int(actual[i_b].size()), ' ') + "\n"; + i_a++; + i_b++; + } + else { + output += std::string(max_a, ' ') + " <-> " + actual[i_b] + + std::string(max_b - int(actual[i_b].size()), ' ') + "\n"; + i_b++; + } + } + else { + output += expected[i_a] + std::string(max_a - int(expected[i_a].size()), ' ') + " <-> " + + std::string(max_b, ' ') + "\n"; + i_a++; + } + } + while (i_a < int(expected.size())) { + output += expected[i_a] + std::string(max_a - int(expected[i_a].size()), ' ') + " <-> " + + std::string(max_b, ' ') + "\n"; + i_a++; + } + while (i_b < int(actual.size())) { + output += std::string(max_a, ' ') + " <-> " + actual[i_b] + + std::string(max_b - int(actual[i_b].size()), ' ') + "\n"; + i_b++; + } + + return output; +} + +} // namespace test_util diff --git a/tests/test_util/diff_string.hpp b/tests/test_util/diff_string.hpp new file mode 100644 index 000000000..a789f39d8 --- /dev/null +++ b/tests/test_util/diff_string.hpp @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2013 Trent Houliston , Jake Woods + * 2014-2023 Trent Houliston + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef TEST_UTIL_DIFF_STRING_HPP +#define TEST_UTIL_DIFF_STRING_HPP + +#include +#include + +namespace test_util { + +/** + * Using an LCS algorithm prints out the two sets of string (expected and actual) side by side to show the + * differences + * + * @param expected the expected series of events + * @param actual the actual series of events + * + * @return a multiline string showing a human output of the difference + */ +std::string diff_string(const std::vector& expected, const std::vector& actual); + +} // namespace test_util + +#endif // TEST_UTIL_DIFF_STRING_HPP diff --git a/tests/test_util/has_ipv6.cpp b/tests/test_util/has_ipv6.cpp new file mode 100644 index 000000000..41d9754b2 --- /dev/null +++ b/tests/test_util/has_ipv6.cpp @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2013 Trent Houliston , Jake Woods + * 2014-2023 Trent Houliston + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "has_ipv6.hpp" + +#include "nuclear" + +namespace test_util { + +bool has_ipv6() { + // See if any interface has an ipv6 address + auto ifaces = NUClear::util::network::get_interfaces(); + return std::any_of(ifaces.begin(), ifaces.end(), [](const auto& iface) { + return iface.ip.sock.sa_family == AF_INET6; + }); +} + +} // namespace test_util diff --git a/tests/test_util/has_ipv6.hpp b/tests/test_util/has_ipv6.hpp new file mode 100644 index 000000000..eb852fc46 --- /dev/null +++ b/tests/test_util/has_ipv6.hpp @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2013 Trent Houliston , Jake Woods + * 2014-2023 Trent Houliston + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TEST_UTIL_HAS_IPV6_HPP +#define TEST_UTIL_HAS_IPV6_HPP + +namespace test_util { + +bool has_ipv6(); + +} // namespace test_util + +#endif // TEST_UTIL_HAS_IPV6_HPP diff --git a/tests/test_util/lcs.hpp b/tests/test_util/lcs.hpp new file mode 100644 index 000000000..d42bda4d3 --- /dev/null +++ b/tests/test_util/lcs.hpp @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2013 Trent Houliston , Jake Woods + * 2014-2023 Trent Houliston + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef TEST_UTIL_LCS_HPP +#define TEST_UTIL_LCS_HPP + +#include +#include +#include + +namespace test_util { + +/** + * Longest common subsequence algorithm that returns which elements from a and b form the common subsequence. + * + * This algorithm compares two lists and finds the longest subsequence you can make that is included in both of the + * lists. It returns a pair of bool vectors that indicate which elements are a part of the subsequence. + * + * @tparam T the type of the elements to be compared + * + * @param a the first list to compare + * @param b the second list to compare + * + * @return two vectors of bools indicating which elements participate in the longest common subexpression + */ +template +std::pair, std::vector> lcs(const std::vector& a, const std::vector& b) { + + // Start with nothing matching + std::vector match_a(a.size(), false); + std::vector match_b(b.size(), false); + + // Nothing matches if one is empty + if (a.empty() || b.empty()) { + return std::make_pair(match_a, match_b); + } + + // Directions matrix for dynamic algorithm + // 0x1 = diagonal, 0x2 = left, 0x4 = top + std::vector> directions(a.size(), std::vector(b.size(), 0)); + + const int insert_weight = 3; + std::vector last_weights(a.size(), 0); + std::vector curr_weights(a.size(), 0); + for (int i = 0, weight = insert_weight; i < int(a.size()); ++i, weight += insert_weight) { + last_weights[i] = weight; + } + + for (int y = 0; y < int(b.size()); ++y) { + for (int x = 0; x < int(a.size()); ++x) { + // Calculate the weights + const int weight_from_left = x == 0 ? (y + 2) * insert_weight : curr_weights[x - 1] + insert_weight; + const int weight_from_top = last_weights[x] + insert_weight; + const int weight_from_diagonal = + a[x] == b[y] ? (x == 0 ? (y + 1) * insert_weight : last_weights[x - 1]) : 0x7FFFFFFF; + + // Find the smallest weight + const int min_weight = std::min(std::min(weight_from_left, weight_from_top), weight_from_diagonal); + curr_weights[x] = min_weight; + + const int direction = (min_weight == weight_from_diagonal ? 0x01 : 0x0) // + | (min_weight == weight_from_left ? 0x02 : 0x0) // + | (min_weight == weight_from_top ? 0x04 : 0x0); // + + directions[x][y] = direction; + } + + // Swap the weights + std::swap(last_weights, curr_weights); + } + + // Rewrite directions(y,x) to match_a[i] and match_b[i]. + int x = int(a.size()) - 1; + int y = int(b.size()) - 1; + int i_a = x; + int i_b = y; + while (x >= 0 && y >= 0) { + const int& direction = directions[x][y]; + if (direction & 0x01) { + match_a[i_a--] = true; + match_b[i_b--] = true; + --x; + --y; + } + else if (direction & 0x02) { + --i_a; + --x; + } + else { // (direction & 0x04) + --i_b; + --y; + } + } + + return std::make_pair(match_a, match_b); +} + +} // namespace test_util + +#endif // TEST_UTIL_LCS_HPP diff --git a/tests/util/network/resolve.cpp b/tests/util/network/resolve.cpp new file mode 100644 index 000000000..e965f0faf --- /dev/null +++ b/tests/util/network/resolve.cpp @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2013 Trent Houliston , Jake Woods + * 2014-2023 Trent Houliston + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include "util/network/resolve.hpp" + +#include + +TEST_CASE("resolve function returns expected socket address", "[util][network][resolve]") { + + SECTION("IPv4 address") { + const std::string address = "127.0.0.1"; + const uint16_t port = 80; + + const auto result = NUClear::util::network::resolve(address, port); + + REQUIRE(result.sock.sa_family == AF_INET); + REQUIRE(ntohs(result.ipv4.sin_port) == port); + REQUIRE(ntohl(result.ipv4.sin_addr.s_addr) == INADDR_LOOPBACK); + } + + SECTION("IPv6 address") { + const std::string address = "::1"; + const uint16_t port = 80; + + const auto result = NUClear::util::network::resolve(address, port); + + REQUIRE(result.sock.sa_family == AF_INET6); + REQUIRE(ntohs(result.ipv6.sin6_port) == port); + // Check each byte of the address except the last one is 0 + for (int i = 0; i < 15; i++) { + REQUIRE(result.ipv6.sin6_addr.s6_addr[i] == 0); + } + // Last byte should be 1 + REQUIRE(result.ipv6.sin6_addr.s6_addr[15] == 1); + } + + SECTION("Hostname") { + const std::string address = "localhost"; + const uint16_t port = 80; + + const auto result = NUClear::util::network::resolve(address, port); + + // Check that the returned socket address matches the expected address and port + REQUIRE((result.sock.sa_family == AF_INET || result.sock.sa_family == AF_INET6)); + + // Localhost could return an ipv4 or ipv6 address + if (result.sock.sa_family == AF_INET) { + REQUIRE(ntohs(result.ipv4.sin_port) == port); + REQUIRE(ntohl(result.ipv4.sin_addr.s_addr) == INADDR_LOOPBACK); + } + else { + REQUIRE(ntohs(result.ipv6.sin6_port) == port); + // Check each byte of the address except the last one is 0 + for (int i = 0; i < 15; i++) { + REQUIRE(result.ipv6.sin6_addr.s6_addr[i] == 0); + } + // Last byte should be 1 + REQUIRE(result.ipv6.sin6_addr.s6_addr[15] == 1); + } + } + + SECTION("IPv6 address with mixed case letters") { + const std::string address = "2001:0DB8:Ac10:FE01:0000:0000:0000:0000"; + const uint16_t port = 80; + + const auto result = NUClear::util::network::resolve(address, port); + + REQUIRE(result.sock.sa_family == AF_INET6); + REQUIRE(ntohs(result.ipv6.sin6_port) == port); + REQUIRE(result.ipv6.sin6_addr.s6_addr[0] == 0x20); + REQUIRE(result.ipv6.sin6_addr.s6_addr[1] == 0x01); + REQUIRE(result.ipv6.sin6_addr.s6_addr[2] == 0x0d); + REQUIRE(result.ipv6.sin6_addr.s6_addr[3] == 0xb8); + REQUIRE(result.ipv6.sin6_addr.s6_addr[4] == 0xac); + REQUIRE(result.ipv6.sin6_addr.s6_addr[5] == 0x10); + REQUIRE(result.ipv6.sin6_addr.s6_addr[6] == 0xfe); + REQUIRE(result.ipv6.sin6_addr.s6_addr[7] == 0x01); + REQUIRE(result.ipv6.sin6_addr.s6_addr[8] == 0x00); + REQUIRE(result.ipv6.sin6_addr.s6_addr[9] == 0x00); + REQUIRE(result.ipv6.sin6_addr.s6_addr[10] == 0x00); + REQUIRE(result.ipv6.sin6_addr.s6_addr[11] == 0x00); + REQUIRE(result.ipv6.sin6_addr.s6_addr[12] == 0x00); + REQUIRE(result.ipv6.sin6_addr.s6_addr[13] == 0x00); + REQUIRE(result.ipv6.sin6_addr.s6_addr[14] == 0x00); + REQUIRE(result.ipv6.sin6_addr.s6_addr[15] == 0x00); + } + + SECTION("Hostname with valid IPv4 address") { + const std::string address = "ipv4.google.com"; + const uint16_t port = 80; + + const auto result = NUClear::util::network::resolve(address, port); + + REQUIRE(result.sock.sa_family == AF_INET); + REQUIRE(ntohs(result.ipv4.sin_port) == port); + REQUIRE(ntohl(result.ipv4.sin_addr.s_addr) != 0); + } + +// For some reason windows hates this test and I'm not sure what to check to see if a windows instance can do this +#ifndef _WIN32 + SECTION("Hostname with valid IPv6 address") { + const std::string address = "ipv6.google.com"; + const uint16_t port = 80; + + const auto result = NUClear::util::network::resolve(address, port); + + REQUIRE(result.sock.sa_family == AF_INET6); + REQUIRE(ntohs(result.ipv6.sin6_port) == port); + + // Check if all components are zero + bool nonzero = false; + for (int i = 0; i < 15; i++) { + nonzero |= result.ipv6.sin6_addr.s6_addr[i] != 0; + } + + REQUIRE(nonzero); + } +#endif + + SECTION("Invalid address") { + const std::string address = "this.url.is.invalid"; + const uint16_t port = 12345; + + // Check that the function throws a std::runtime_error with the appropriate message + REQUIRE_THROWS(NUClear::util::network::resolve(address, port)); + } +}