Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into Jake/Movement+KeyInput
Browse files Browse the repository at this point in the history
  • Loading branch information
Legac3e committed Jan 28, 2024
2 parents e194205 + 3ac64c5 commit 3bc9c8e
Show file tree
Hide file tree
Showing 11 changed files with 233 additions and 73 deletions.
29 changes: 16 additions & 13 deletions .clang-tidy
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
--- Checks: 'clang-analyzer-*,
concurrency-*,
cppcoreguidelines-*,
-cppcoreguidelines-non-private-member-variables-in-classes,
misc-*,
-misc-non-private-member-variables-in-classes,
modernize-*,
-modernize-use-trailing-return-type,
performance-*,
portability-*,
readability-*,
-readability-identifier-length,
-readability-uppercase-literal-suffix -readability-magic-numbers ...
---
Checks: 'clang-analyzer-*,
concurrency-*,
cppcoreguidelines-*,
-cppcoreguidelines-non-private-member-variables-in-classes,
misc-*,
-misc-non-private-member-variables-in-classes,
modernize-*,
-modernize-use-trailing-return-type,
performance-*,
portability-*,
readability-*,
-readability-identifier-length,
-readability-uppercase-literal-suffix
-readability-magic-numbers'
...
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ if(EXISTS "${compile_commands_path}")
message(STATUS "Copying compile_commands.json from ${compile_commands_path}")
file(COPY "${compile_commands_path}" DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}")
endif()

enable_testing()
add_test(NAME DogTalesUnitTests COMMAND DogTales -t)
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
5 changes: 5 additions & 0 deletions DogTales/src/services/service.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#pragma once
#include <bave/core/polymorphic.hpp>

/// \brief Base class for all services.
class IService : public bave::Polymorphic {};
90 changes: 90 additions & 0 deletions DogTales/src/services/services.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#pragma once
#include <bave/core/not_null.hpp>
#include <bave/core/ptr.hpp>
#include <src/fatal_error.hpp>
#include <src/services/service.hpp>
#include <memory>
#include <mutex>
#include <typeindex>
#include <unordered_map>

/// \brief Concept constraining Type to a subclass of IService.
template <typename Type>
concept ServiceT = std::derived_from<Type, IService>;

/// \brief Container of stored services.
///
/// Intended usage:
/// 1. Own an instance in main / app Driver.
/// 2. Bind services to subclasses. (From and To being the same type is also OK.)
/// 3. Pass a const reference of Services to dependencies
/// Point 3 ensures dependencies cannot bind new / remove existing services,
/// but can locate and use already bound ones.
class Services {
public:
/// \brief Bind a service instance to a type (From).
/// \param service Concrete instance of service to bind.
/// \pre service must not be null, From must not already be bound.
template <ServiceT From, std::derived_from<From> To>
void bind(std::unique_ptr<To> service) {
if (!service) { throw FatalError{"Attempt to bind null service"}; }
static auto const index = std::type_index{typeid(From)};
auto lock = std::scoped_lock{m_mutex};
if (m_services.contains(index)) { throw FatalError{"Attempt to bind duplicate service"}; }
m_services.insert_or_assign(index, std::move(service));
}

/// \brief Remove a bound service instance. Type need not actually be bound.
template <ServiceT Type>
void remove() {
static auto const index = std::type_index{typeid(Type)};
auto lock = std::scoped_lock{m_mutex};
m_services.erase(index);
}

/// \brief Remove all bound service instances.
void remove_all() {
auto lock = std::scoped_lock{m_mutex};
m_services.clear();
}

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

/// \brief Locate a service instance if bound.
/// \returns Pointer to service instance if bound, else nullptr.
template <ServiceT Type>
[[nodiscard]] auto find() const -> bave::Ptr<Type> {
static auto const index = std::type_index{typeid(Type)};
auto lock = std::scoped_lock{m_mutex};
if (auto const it = m_services.find(index); it != m_services.end()) {
return static_cast<Type*>(it->second.get());
}
return {};
}

/// \brief Obtain a bound service.
/// \returns Mutable reference to bound service.
/// \pre Type must be bound.
template <ServiceT Type>
[[nodiscard]] auto get() const -> Type& {
auto ret = find<Type>();
if (!ret) { throw FatalError{"Service not found"}; }
return *ret;
}

/// \brief Obtain the count of bound services.
/// \returns Count of bound services.
[[nodiscard]] auto size() const -> std::size_t {
auto lock = std::scoped_lock{m_mutex};
return m_services.size();
}

private:
std::unordered_map<std::type_index, std::unique_ptr<IService>> m_services{};
mutable std::mutex m_mutex{};
};
43 changes: 22 additions & 21 deletions tests/test/test.cpp → DogTales/src/tests/test.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#include <test/test.hpp>
#include <fmt/format.h>
#include <src/tests/test.hpp>
#include <filesystem>
#include <format>
#include <iostream>
#include <vector>

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 << fmt::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,40 +24,40 @@ 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 = {};
try {
test.run();
} catch (Assert) {
} catch (std::exception const& e) {
std::cerr << std::format("exception caught: {}\n", e.what());
std::cerr << fmt::format("exception caught: {}\n", e.what());
g_failed = true;
}
if (g_failed) {
std::cerr << std::format("[FAILED] {}\n", test.get_name());
std::cerr << fmt::format("[FAILED] {}\n", test.get_name());
return false;
}
std::cout << std::format("[passed] {}\n", test.get_name());
std::cout << fmt::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
74 changes: 74 additions & 0 deletions DogTales/src/tests/test_services.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#include <bave/core/pinned.hpp>
#include <src/services/services.hpp>
#include <src/tests/test.hpp>

namespace {
namespace one {
struct Foo : IService {
int value{};
};
} // namespace one

namespace two {
struct Foo : IService {
int value{};
};
} // namespace two

ADD_TEST(Services_TypeIntegrity) {
auto services = Services{};

auto foo_one = std::make_unique<one::Foo>();
foo_one->value = 1;
services.bind<one::Foo>(std::move(foo_one));

auto foo_two = std::make_unique<two::Foo>();
foo_two->value = 2;
services.bind<two::Foo>(std::move(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);
}

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

EXPECT(thrown);
}

ADD_TEST(Services_DuplicateException) {
auto services = Services{};

services.bind<one::Foo>(std::make_unique<one::Foo>());

auto thrown = false;
try {
services.bind<one::Foo>(std::make_unique<one::Foo>());
} catch (FatalError const&) { thrown = true; }

EXPECT(thrown);
}

ADD_TEST(Services_BindSubclass) {
auto services = Services{};

struct Interface : IService {
virtual auto get_value() -> int = 0;
};

struct Concrete : Interface {
auto get_value() -> int final { return 42; }
};

services.bind<Interface>(std::make_unique<Concrete>());

ASSERT(services.contains<Interface>());
EXPECT(services.get<Interface>().get_value() == 42);
}
} // 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 3bc9c8e

Please sign in to comment.