Skip to content

Commit

Permalink
Add services un/provision and access.
Browse files Browse the repository at this point in the history
- Move test framework into exe target.
- Add CLI option to run tests instead of the game.

Unit tests should ideally be in a separate executable, but that would require refactoring all the code into a library that both the game exe and unit tests exe would link to. This monolithic approach keeps things simple at the cost of game binary bloat.
  • Loading branch information
karnkaul committed Jan 28, 2024
1 parent bf8def0 commit 236e4a0
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 56 deletions.
10 changes: 10 additions & 0 deletions DogTales/src/fatal_error.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#pragma once
#include <fmt/format.h>
#include <stdexcept>

class FatalError : public std::runtime_error {
public:
template <typename... Args>
explicit FatalError(fmt::format_string<Args...> fmt, Args&&... args)
: std::runtime_error(fmt::format(fmt, std::forward<Args>(args)...)) {}
};
5 changes: 5 additions & 0 deletions DogTales/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <bave/desktop_app.hpp>
#include <src/build_version.hpp>
#include <src/dogtales.hpp>
#include <src/tests/test.hpp>
#include <iostream>

namespace {
Expand All @@ -13,7 +14,9 @@ auto parse_args(int const argc, char const* const* argv) -> std::optional<int> {
auto options = clap::Options{std::move(name), std::move(description), std::move(version)};

auto show_bave_version = false;
auto run_tests = false;
options.flag(show_bave_version, "bave-version", "show bave version");
options.flag(run_tests, "t,run-tests", "run DogTales tests");

auto const result = options.parse(argc, argv);
if (clap::should_quit(result)) { return clap::return_code(result); }
Expand All @@ -23,6 +26,8 @@ auto parse_args(int const argc, char const* const* argv) -> std::optional<int> {
return EXIT_SUCCESS;
}

if (run_tests) { return test::run_tests(); }

return {};
}

Expand Down
39 changes: 39 additions & 0 deletions DogTales/src/services/services.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#include <src/services/services.hpp>
#include <mutex>
#include <unordered_map>

namespace {
struct {
std::unordered_map<std::type_index, void*> services{};
std::mutex mutex{};
} g_data{}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace

namespace services {
void detail::provide(std::type_index const index, void* obj) {
auto lock = std::scoped_lock{g_data.mutex};
if (g_data.services.contains(index)) { throw FatalError{"Duplicate service instance"}; }
g_data.services.insert_or_assign(index, obj);
}

void detail::unprovide(std::type_index const index) {
auto lock = std::scoped_lock{g_data.mutex};
g_data.services.erase(index);
}

auto detail::find(std::type_index const index) -> void* {
auto lock = std::scoped_lock{g_data.mutex};
if (auto const it = g_data.services.find(index); it != g_data.services.end()) { return it->second; }
return {};
}
} // namespace services

void services::unprovide_all() {
auto lock = std::scoped_lock{g_data.mutex};
g_data.services.clear();
}

auto services::get_count() -> std::size_t {
auto lock = std::scoped_lock{g_data.mutex};
return g_data.services.size();
}
91 changes: 91 additions & 0 deletions DogTales/src/services/services.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#pragma once
#include <bave/core/not_null.hpp>
#include <bave/core/ptr.hpp>
#include <src/fatal_error.hpp>
#include <typeindex>

/// \brief Concept for a serviceable type: it must be pinned since its address is stored.
template <typename Type>
concept ServiceT = !std::is_move_assignable_v<Type> && !std::is_copy_assignable_v<Type>;

/// \brief Services are effectively globals, since any code anywhere can poke at them.
/// The design employed here minimizes the negative effects of globals via:
/// 1. Type safe service access / provision / unprovision.
/// 2. No ownership of service objects.
/// 3. Consolidated storage of all such globals (as opposed to singletons).
/// 4. Batch removal of all provided services.
///
/// Point 2 means that a service instance must actually be (uniquely) owned elsewhere.
/// This provides deterministic lifetime of all services,
/// as opposed to global storage, which pushes lifetimes beyond main's scope, and
/// makes the initialization/destruction order unspecified wrt each other.
namespace services {
/// \brief Internal usage, do not use anything in detail directly.
namespace detail {
void provide(std::type_index index, void* obj);
void unprovide(std::type_index index);
[[nodiscard]] auto find(std::type_index index) -> void*;
} // namespace detail

/// \brief Provide a service by registering its address.
/// \param obj Address of service object.
/// \pre Type must not already be provided.
template <ServiceT Type>
void provide(bave::NotNull<Type*> obj) {
detail::provide(std::type_index{typeid(Type)}, obj.get());
}

/// \brief Unprovide a service. Safe to call even if service hasn't been provided.
template <ServiceT Type>
void unprovide() {
detail::unprovide(std::type_index{typeid(Type)});
}

/// \brief Unregister all provided services.
void unprovide_all();

/// \brief Obtain the count of provided services.
[[nodiscard]] auto get_count() -> std::size_t;

/// \brief Locate a service if provided.
/// \returns Pointer to service instance if provided, nullptr otherwise.
template <ServiceT Type>
[[nodiscard]] auto find() -> bave::Ptr<Type> {
return static_cast<Type*>(detail::find(std::type_index{typeid(Type)}));
}

/// \brief Check if a service type has been provided.
/// \returns true if provided.
template <ServiceT Type>
[[nodiscard]] auto contains() -> bool {
return find<Type>() != nullptr;
}

/// \brief Locate a provided service.
/// \returns Reference to provided service instance.
/// \pre Type must have been provided.
template <ServiceT Type>
[[nodiscard]] auto get() -> Type& {
auto* ret = find<Type>();
if (ret == nullptr) { throw FatalError{"Requested service not present"}; }
return *ret;
}
} // namespace services

/// \brief CRTP base class for scoped (RAII) service types.
///
/// Usage:
/// struct Foo : CrtpService<Foo> { /*...*/ };
/// Creating a Foo instance will auto-provide Foo service.
/// It will be unprovided on destruction.
template <ServiceT Type>
class CrtpService {
public:
CrtpService(CrtpService&&) = delete;
CrtpService(CrtpService const&) = delete;
auto operator=(CrtpService&&) -> CrtpService& = delete;
auto operator=(CrtpService const&) -> CrtpService& = delete;

CrtpService() { services::provide<Type>(static_cast<Type*>(this)); }
~CrtpService() { services::unprovide<Type>(); }
};
35 changes: 18 additions & 17 deletions tests/test/test.cpp → DogTales/src/tests/test.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#include <test/test.hpp>
#include <src/tests/test.hpp>
#include <filesystem>
#include <format>
#include <iostream>
Expand All @@ -9,7 +9,8 @@ namespace {
struct Assert {};

void print_failure(std::string_view type, std::string_view expr, std::source_location const& sl) {
std::cerr << std::format(" {} failed: '{}' [{}:{}]\n", type, expr, std::filesystem::path{sl.file_name()}.filename().string(), sl.line());
std::cerr << std::format(" {} failed: '{}' [{}:{}]\n", type, expr,
std::filesystem::path{sl.file_name()}.filename().string(), sl.line());
}

bool g_failed{}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
Expand All @@ -23,20 +24,6 @@ auto get_tests() -> std::vector<Test*>& {
static auto ret = std::vector<Test*>{};
return ret;
}
} // namespace

Test::Test() { get_tests().push_back(this); }

void Test::do_expect(bool pred, std::string_view expr, std::source_location const& location) {
if (pred) { return; }
set_failure("expectation", expr, location);
}

void Test::do_assert(bool pred, std::string_view expr, std::source_location const& location) {
if (pred) { return; }
set_failure("assertion", expr, location);
throw Assert{};
}

auto run_test(Test& test) -> bool {
g_failed = {};
Expand All @@ -54,9 +41,23 @@ auto run_test(Test& test) -> bool {
std::cout << std::format("[passed] {}\n", test.get_name());
return true;
}
} // namespace

Test::Test() { get_tests().push_back(this); }

void Test::do_expect(bool pred, std::string_view expr, std::source_location const& location) {
if (pred) { return; }
set_failure("expectation", expr, location);
}

void Test::do_assert(bool pred, std::string_view expr, std::source_location const& location) {
if (pred) { return; }
set_failure("assertion", expr, location);
throw Assert{};
}
} // namespace test

auto main() -> int {
auto test::run_tests() -> int {
auto ret = EXIT_SUCCESS;
for (auto* test : test::get_tests()) {
if (!run_test(*test)) { ret = EXIT_FAILURE; }
Expand Down
14 changes: 8 additions & 6 deletions tests/test/test.hpp → DogTales/src/tests/test.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,18 @@ class Test {
static void do_expect(bool pred, std::string_view expr, std::source_location const& location);
static void do_assert(bool pred, std::string_view expr, std::source_location const& location);
};

int run_tests();
} // namespace test

#define EXPECT(pred) do_expect(pred, #pred, std::source_location::current()) // NOLINT(cppcoreguidelines-macro-usage)
#define ASSERT(pred) do_assert(pred, #pred, std::source_location::current()) // NOLINT(cppcoreguidelines-macro-usage)

// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define ADD_TEST(Class) \
struct Test_##Class : ::test::Test { \
void run() const final; \
auto get_name() const -> std::string_view final { return #Class; } \
}; \
inline Test_##Class const g_test_##Class{}; \
#define ADD_TEST(Class) \
struct Test_##Class : ::test::Test { \
void run() const final; \
auto get_name() const -> std::string_view final { return #Class; } \
}; \
inline Test_##Class const g_test_##Class{}; \
inline void Test_##Class::run() const
73 changes: 73 additions & 0 deletions DogTales/src/tests/test_services.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#include <bave/core/pinned.hpp>
#include <src/services/services.hpp>
#include <src/tests/test.hpp>

namespace {
namespace one {
struct Foo : bave::Pinned {
int value{};
};
} // namespace one

namespace two {
struct Foo : bave::Pinned {
int value{};
};
} // namespace two

ADD_TEST(ServicesTypeIntegrity) {
auto foo_one = one::Foo{};
foo_one.value = 1;
services::provide<one::Foo>(&foo_one);

auto foo_two = two::Foo{};
foo_two.value = 2;
services::provide<two::Foo>(&foo_two);

ASSERT(services::contains<one::Foo>());
EXPECT(services::get<one::Foo>().value == 1);
ASSERT(services::contains<two::Foo>());
EXPECT(services::get<two::Foo>().value == 2);

services::unprovide_all();
}

ADD_TEST(ServicesGetException) {
auto thrown = false;
try {
[[maybe_unused]] auto& foo = services::get<one::Foo>();
} catch (FatalError const&) { thrown = true; }

EXPECT(thrown);
}

ADD_TEST(ServicesDuplicateException) {
auto foo = one::Foo{};
services::provide<one::Foo>(&foo);

auto thrown = false;
try {
services::provide<one::Foo>(&foo);
} catch (FatalError const&) { thrown = true; }

EXPECT(thrown);
services::unprovide_all();
}

ADD_TEST(ServicesCrtpService) {
struct Bar : CrtpService<Bar> {
int value{};
};

{
auto bar = Bar{};
bar.value = 42;

ASSERT(services::contains<Bar>());
EXPECT(services::get<Bar>().value == 42);
}

EXPECT(!services::contains<Bar>());
services::unprovide_all();
}
} // namespace
22 changes: 0 additions & 22 deletions tests/CMakeLists.txt

This file was deleted.

11 changes: 0 additions & 11 deletions tests/tests/CMakeLists.txt

This file was deleted.

0 comments on commit 236e4a0

Please sign in to comment.