From 37c8d702bd0faf2493ecaecd11a18a51f4c6106d Mon Sep 17 00:00:00 2001 From: BartolomeyKant Date: Thu, 10 Sep 2026 16:55:57 +0500 Subject: [PATCH 1/4] add an env module --- src/aether-objects/env/env.h | 144 ++++++++++++++++++++++++++++++ src/aether-objects/obj/domain.cpp | 6 +- src/aether-objects/obj/domain.h | 7 +- 3 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 src/aether-objects/env/env.h diff --git a/src/aether-objects/env/env.h b/src/aether-objects/env/env.h new file mode 100644 index 0000000..212e145 --- /dev/null +++ b/src/aether-objects/env/env.h @@ -0,0 +1,144 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_OBJECTS_ENV_ENV_H_ +#define AETHER_OBJECTS_ENV_ENV_H_ + +#include +#include +#include +#include +#include + +#include "aether-miscpp/meta/type_index.h" + +namespace ae { + +using EnvId = std::uint32_t; + +static_assert(sizeof(EnvId) == 4); + +/** + * @brief Customizes the identifier used to find a component in an Env. + * + * Specialize this trait for an unqualified component type when a stable custom + * identifier is required. The default is a CRC32 of GetTypeIndex(). Its + * value depends on compiler-generated type names, so it is not portable across + * compilers and CRC32 collisions are possible. Env IDs are process-local + * lookup keys only and must not be persisted. Specializations must be ODR + * consistent throughout a program. + */ +template + requires(!std::is_const_v && !std::is_volatile_v && + !std::is_reference_v) +struct EnvTypeId { + static constexpr EnvId value = static_cast(GetTypeIndex()); +}; + +/** + * \brief Provides lookup access to components in a concrete environment. + */ +class Env { + public: + virtual ~Env() = default; + + /** + * \brief Returns the component of type `T`. + * + * The component must be available in this environment. + */ + template + T& get() { + auto* component = try_get(); + assert(component != nullptr && "Env component is not available"); + if (component == nullptr) { + std::abort(); + } + return *component; + } + + /** + * \brief Returns the component of type `T`, or `nullptr` when unavailable. + */ + template + requires(!std::is_const_v && !std::is_volatile_v && + !std::is_reference_v) + auto try_get() noexcept -> T* { + return static_cast(find_component(EnvTypeId::value)); + } + + protected: + /** + * \brief Finds the live component identified by @p id. + * + * Each concrete environment must support components with unique + * EnvTypeId values. The default values are CRC32 hashes of compiler-generated + * type names and can collide; custom IDs can collide as well. CRC32-based + * default IDs also are not portable across compilers. A collision makes + * component lookup ambiguous, so a concrete environment must choose unique + * IDs for all of its supported components. + * + * Implementations should compare @p id with EnvTypeId::value and return + * the address of the corresponding live component, or nullptr for an + * unknown ID. For a lookup of `T`, the returned pointer must point to an + * object whose exact normalized type is `T` (without cv/ref qualifiers), + * rather than a base or related type. The object must remain alive and at a + * stable address for the entire time callers use the returned pointer. + */ + virtual void* find_component(EnvId id) noexcept = 0; +}; + +/** + * \brief Requires a provider that exposes an Env through get_env(). + */ +template +concept EnvProvider = requires(P const& provider) { + { provider.get_env() } -> std::same_as; +}; + +/** + * \brief Returns the component of type `T` from @p provider's environment. + * + * The provider's get_env() must return an environment, and that environment + * must provide the component. A null environment violates this contract and + * aborts the program. + */ +template +auto get_env(EnvProvider auto const& provider) -> T& { + auto* env = provider.get_env(); + assert(env != nullptr && "Env provider does not have an environment"); + if (env == nullptr) { + std::abort(); + } + return env->template get(); +} + +/** + * \brief Returns the component of type `T` from @p provider's environment. + * + * Returns `nullptr` when the provider's get_env() returns `nullptr` or its + * environment does not provide the component. + */ +template +auto try_get_env(EnvProvider auto const& provider) -> T* +{ + auto* env = provider.get_env(); + return env == nullptr ? nullptr : env->template try_get(); +} + +} // namespace ae + +#endif // AETHER_OBJECTS_ENV_ENV_H_ diff --git a/src/aether-objects/obj/domain.cpp b/src/aether-objects/obj/domain.cpp index caf0312..f6075d9 100644 --- a/src/aether-objects/obj/domain.cpp +++ b/src/aether-objects/obj/domain.cpp @@ -87,8 +87,10 @@ std::unique_ptr DomainGraph::GetWriter( return writer; } -Domain::Domain(IDomainStorage& storage) - : storage_{&storage}, registry_{&Registry::GetRegistry()} {} +Domain::Domain(IDomainStorage& storage) : Domain(storage, nullptr) {} + +Domain::Domain(IDomainStorage& storage, Env* env) + : storage_{&storage}, registry_{&Registry::GetRegistry()}, env_{env} {} Ptr Domain::ConstructObj(Factory const& factory, ObjId obj_id) { Ptr o = factory.create(); diff --git a/src/aether-objects/obj/domain.h b/src/aether-objects/obj/domain.h index c760c88..b41941e 100644 --- a/src/aether-objects/obj/domain.h +++ b/src/aether-objects/obj/domain.h @@ -22,7 +22,6 @@ #include #include #include -#include #include "aether-miscpp/domain_visitor/domain_visitor.h" #include "aether-miscpp/serialization/binary_archive.h" @@ -30,6 +29,7 @@ #include "aether-objects/ptr/ptr_view.h" +#include "aether-objects/env/env.h" #include "aether-objects/obj/idomain_storage.h" #include "aether-objects/obj/obj_id.h" #include "aether-objects/obj/registry.h" @@ -135,6 +135,10 @@ class Domain { public: explicit Domain(IDomainStorage& storage); + Domain(IDomainStorage& storage, Env* env); + + /// \brief Returns this domain's optional environment. + Env* get_env() const { return env_; } // Search for the object by obj_id. Ptr Find(ObjId obj_id) const; @@ -153,6 +157,7 @@ class Domain { IDomainStorage* storage_; Registry* registry_; + Env* env_{}; std::map> id_objects_; }; From 4a1ffebe3f82a06d6d2f9729903745de3c561cdd Mon Sep 17 00:00:00 2001 From: BartolomeyKant Date: Thu, 10 Sep 2026 16:56:00 +0500 Subject: [PATCH 2/4] add tests for env --- tests/test-object-system/CMakeLists.txt | 1 + tests/test-object-system/main.cpp | 2 + tests/test-object-system/test-env.cpp | 211 ++++++++++++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 tests/test-object-system/test-env.cpp diff --git a/tests/test-object-system/CMakeLists.txt b/tests/test-object-system/CMakeLists.txt index 02c2261..75e67b8 100644 --- a/tests/test-object-system/CMakeLists.txt +++ b/tests/test-object-system/CMakeLists.txt @@ -17,6 +17,7 @@ cmake_minimum_required( VERSION 3.16 ) list(APPEND test_srcs main.cpp + test-env.cpp test-obj-create.cpp test-obj-ptr-forward-decl.cpp test-update-objects.cpp diff --git a/tests/test-object-system/main.cpp b/tests/test-object-system/main.cpp index 35fa809..4f988c0 100644 --- a/tests/test-object-system/main.cpp +++ b/tests/test-object-system/main.cpp @@ -20,11 +20,13 @@ void setUp() {} void tearDown() {} extern int run_test_object_create(); +extern int run_test_env(); extern int run_test_update_objects(); extern int run_test_version_iterator(); int main() { int res{}; + res += run_test_env(); res += run_test_object_create(); res += run_test_version_iterator(); res += run_test_update_objects(); diff --git a/tests/test-object-system/test-env.cpp b/tests/test-object-system/test-env.cpp new file mode 100644 index 0000000..1fc789c --- /dev/null +++ b/tests/test-object-system/test-env.cpp @@ -0,0 +1,211 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include + +#include "aether-objects/env/env.h" +#include "aether-objects/obj/domain.h" +#include "map_domain_storage.h" + +namespace ae::test_env { + +/// \brief Component whose default type-derived ID is used for lookup tests. +struct DefaultComponent { + int value{}; +}; + +/// \brief Component whose explicit ID specialization is used for lookup tests. +struct CustomComponent { + int value{}; +}; + +/// \brief Explicit environment ID assigned to CustomComponent. +inline constexpr EnvId kCustomComponentId = 0x12345678U; + +} // namespace ae::test_env + +namespace ae { +template <> +struct EnvTypeId { + static constexpr EnvId value = test_env::kCustomComponentId; +}; + +} // namespace ae + +namespace ae::test_env { + +/// \brief Environment fixture that exposes default and custom-ID components. +class TestEnv final : public Env { + public: + DefaultComponent default_component{.value = 1}; + CustomComponent custom_component{.value = 2}; + + protected: + void* find_component(EnvId id) noexcept override { + if (id == EnvTypeId::value) { + return &default_component; + } + if (id == EnvTypeId::value) { + return &custom_component; + } + return nullptr; + } +}; + +/// \brief Environment fixture that verifies private components remain usable. +class PrivateComponentEnv final : public Env { + public: + /// \brief Verifies this fixture can look up its private component type. + void VerifyAccess() { + TEST_ASSERT_EQUAL(3, get().value); + auto& env = static_cast(*this); + TEST_ASSERT_EQUAL_PTR(&component_, env.try_get()); + } + + private: + struct PrivateComponent { + int value{3}; + }; + + protected: + void* find_component(EnvId id) noexcept override { + return id == EnvTypeId::value ? &component_ : nullptr; + } + + private: + PrivateComponent component_; +}; + +/// \brief Provider fixture that returns a nullable environment and counts +/// calls. +struct Provider { + Env* env{}; + mutable int calls{}; + + Env* get_env() const { + ++calls; + return env; + } +}; + +/// \brief Invalid provider fixture that returns a const environment pointer. +struct ConstEnvProvider { + Env const* get_env() const; +}; + +/// \brief Invalid provider fixture that returns an environment reference. +struct ReferenceEnvProvider { + Env& get_env() const; +}; + +/// \brief Type convertible to an environment pointer but not an environment +/// pointer. +struct EnvPointerConvertible { + explicit operator Env*() const; +}; + +/// \brief Invalid provider fixture that returns a pointer-convertible value. +struct ConvertibleEnvProvider { + EnvPointerConvertible get_env() const; +}; + +template +concept HasTryGet = requires(T const& provider) { provider.try_get(); }; + +template +concept HasEnvTryGet = requires(Env& env) { env.template try_get(); }; + +static_assert(sizeof(EnvId) == 4); +static_assert(std::same_as); +static_assert(EnvTypeId::value == kCustomComponentId); +static_assert(EnvProvider); +static_assert(!EnvProvider); +static_assert(!EnvProvider); +static_assert(!EnvProvider); +static_assert(EnvProvider); +static_assert(!HasTryGet); +static_assert(!std::is_invocable_v< + decltype(&Env::template get), Env const&>); +static_assert(!std::is_invocable_v< + decltype(&Env::template try_get), Env const&>); +static_assert(HasEnvTryGet); +static_assert(std::same_as< + decltype(std::declval().template get()), + DefaultComponent&>); + +/// \brief Verifies typed lookup finds registered components and misses unknown +/// IDs. +void test_EnvComponentLookup() { + TestEnv env; + TEST_ASSERT_EQUAL(1, env.get().value); + TEST_ASSERT_EQUAL_PTR(&env.default_component, + env.try_get()); + TEST_ASSERT_EQUAL(1, env.get().value); + TEST_ASSERT_EQUAL(2, env.get().value); + TEST_ASSERT_NULL(env.try_get()); + + PrivateComponentEnv private_env; + private_env.VerifyAccess(); +} + +/// \brief Verifies providers expose environments for direct component lookup. +void test_EnvHelpers() { + TestEnv env; + Provider const provider{.env = &env}; + TEST_ASSERT_EQUAL(1, get_env(provider).value); + TEST_ASSERT_EQUAL(1, provider.calls); + TEST_ASSERT_EQUAL(1, get_env(provider).value); + TEST_ASSERT_EQUAL(2, provider.calls); + TEST_ASSERT_EQUAL_PTR(&env.custom_component, + try_get_env(provider)); + TEST_ASSERT_EQUAL(3, provider.calls); + TEST_ASSERT_EQUAL_PTR(&env.custom_component, + provider.get_env()->try_get()); + TEST_ASSERT_EQUAL(4, provider.calls); + + Provider const null_provider{}; + TEST_ASSERT_NULL(null_provider.get_env()); + TEST_ASSERT_EQUAL(1, null_provider.calls); +} + +/// \brief Verifies Domain returns its optional environment through get_env and +/// supports EnvProvider component lookup. +void test_DomainEnv() { + MapDomainStorage storage; + Domain const without_env{storage}; + TEST_ASSERT_NULL(without_env.get_env()); + + TestEnv env; + Domain const with_env{storage, &env}; + TEST_ASSERT_EQUAL_PTR(&env, with_env.get_env()); + TEST_ASSERT_EQUAL_PTR(&env.default_component, + with_env.get_env()->try_get()); +} + +} // namespace ae::test_env + +/// \brief Runs the environment component and provider contract test suite. +int run_test_env() { + UNITY_BEGIN(); + RUN_TEST(ae::test_env::test_EnvComponentLookup); + RUN_TEST(ae::test_env::test_EnvHelpers); + RUN_TEST(ae::test_env::test_DomainEnv); + return UNITY_END(); +} From 588e6953f0249edaaddb96e263146e56bc2b2d7a Mon Sep 17 00:00:00 2001 From: BartolomeyKant Date: Fri, 11 Sep 2026 10:53:03 +0500 Subject: [PATCH 3/4] add call Loaded and Saved after object was loaded and saved --- src/aether-objects/obj/domain.cpp | 40 ++++++++++++----- src/aether-objects/obj/domain.h | 57 ++++++++++++++++++------- src/aether-objects/obj/obj_ptr_base.cpp | 7 +-- src/aether-objects/obj/registrar.h | 24 ++++++++--- src/aether-objects/obj/registry.h | 8 ++-- 5 files changed, 99 insertions(+), 37 deletions(-) diff --git a/src/aether-objects/obj/domain.cpp b/src/aether-objects/obj/domain.cpp index f6075d9..3abf8f4 100644 --- a/src/aether-objects/obj/domain.cpp +++ b/src/aether-objects/obj/domain.cpp @@ -27,22 +27,34 @@ namespace ae { DomainGraph::DomainGraph(Domain* domain) : domain(domain) { assert(domain); } Ptr DomainGraph::LoadRoot(ObjId obj_id) { - if (!obj_id.is_valid()) { + Ptr ptr; + if (LoadRoot(obj_id, ptr).IsErr()) { return {}; } + return ptr; +} + +seri::SeriResult DomainGraph::LoadRoot(ObjId obj_id, Ptr& ptr) { + if (!obj_id.is_valid()) { + return Error{seri::read_error}; + } // if already loaded - if (auto obj = domain->Find(obj_id); obj) { - return obj; + if (ptr = domain->Find(obj_id); ptr) { + return Ok{seri::good}; } auto* factory = domain->GetMostRelatedFactory(obj_id); if (factory == nullptr) { - return {}; + return Error{seri::read_error}; } - auto ptr = domain->ConstructObj(*factory, obj_id); - factory->load(this, ptr, obj_id); - return ptr; + ptr = domain->ConstructObj(*factory, obj_id); + auto result = factory->load(this, ptr, obj_id); + if (result && result->IsErr()) { + domain->RemoveObject(ptr.get()); + ptr.Reset(); + } + return result.value_or(Ok{seri::good}); } Ptr DomainGraph::LoadCopyImpl(ObjId ref_id, ObjId copy_id) { @@ -62,18 +74,24 @@ Ptr DomainGraph::LoadCopyImpl(ObjId ref_id, ObjId copy_id) { auto ptr = domain->ConstructObj(*factory, copy_id); // load new object with ref_id - factory->load(this, ptr, ref_id); + auto result = factory->load(this, ptr, ref_id); + if (result && result->IsErr()) { + domain->RemoveObject(ptr.get()); + return {}; + } return ptr; } -void DomainGraph::SaveRoot(Ptr const& ptr, ObjId obj_id) { +seri::SeriResult DomainGraph::SaveRoot(Ptr const& ptr, ObjId obj_id) { if (!ptr) { - return; + return Error{seri::write_error}; } if (auto* factory = domain->FindClassFactory(ptr->GetClassId()); factory != nullptr) { - factory->save(this, ptr, obj_id); + auto result = factory->save(this, ptr, obj_id); + return result.value_or(Ok{seri::good}); } + return Error{seri::write_error}; } DomainLoad DomainGraph::GetReader(DomainQuery const& query) { diff --git a/src/aether-objects/obj/domain.h b/src/aether-objects/obj/domain.h index b41941e..dbeab19 100644 --- a/src/aether-objects/obj/domain.h +++ b/src/aether-objects/obj/domain.h @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -36,9 +37,17 @@ #include "aether-objects/obj/version_iterator.h" namespace ae { + +namespace domain_internal { +inline constexpr seri::SeriError missing_version{ + .error_code = 1000, .message = "Object version is missing"}; +} // namespace domain_internal + class Obj; class Domain; class DomainGraph; +template +class Registrar; struct DomainCycleDetector { struct Node { @@ -104,8 +113,9 @@ class DomainGraph { // Load saved state of object. Ptr LoadRoot(ObjId obj_id); + seri::SeriResult LoadRoot(ObjId obj_id, Ptr& ptr); // Save state of object. - void SaveRoot(Ptr const& ptr, ObjId obj_id); + seri::SeriResult SaveRoot(Ptr const& ptr, ObjId obj_id); // Load a copy of object template Ptr LoadCopy(ObjId ref_id, ObjId copy_id); @@ -113,12 +123,12 @@ class DomainGraph { Ptr LoadCopyImpl(ObjId ref_id, ObjId copy_id); template - seri::SeriResult Load(T& obj, ObjId obj_id); + std::optional Load(T& obj, ObjId obj_id); template seri::SeriResult LoadVersion(Version version, T& obj, ObjId obj_id); template - seri::SeriResult Save(T const& obj, ObjId obj_id); + std::optional Save(T const& obj, ObjId obj_id); template seri::SeriResult SaveVersion(Version version, T const& obj, ObjId obj_id); @@ -130,6 +140,11 @@ class DomainGraph { std::unique_ptr GetWriter(DomainQuery const& query); }; +inline bool IsMissingVersion(seri::SeriResult const& result) { + return result.IsErr() && result.error().error_code == + domain_internal::missing_version.error_code; +} + class Domain { friend class DomainGraph; @@ -168,19 +183,24 @@ Ptr DomainGraph::LoadCopy(ObjId ref_id, ObjId copy_id) { } template -seri::SeriResult DomainGraph::Load(T& obj, ObjId obj_id) { +std::optional DomainGraph::Load(T& obj, ObjId obj_id) { if (!cycle_detector.Add(T::kClassId, obj_id)) { - return Ok{seri::good}; + return std::nullopt; } if constexpr (HasAnyVersionedLoad::value) { - auto result = seri::SeriResult{Ok{seri::good}}; + auto result = seri::SeriResult{Error{domain_internal::missing_version}}; + auto is_missing = true; version_iterator( - obj, [this, obj_id, &result](auto version, auto& obj) { - if (result.IsErr()) { + obj, [this, obj_id, &result, &is_missing](auto version, auto& obj) { + if (!is_missing && result.IsErr()) { return; } - result = this->LoadVersion(version, obj, obj_id); + auto version_result = this->LoadVersion(version, obj, obj_id); + is_missing = IsMissingVersion(version_result); + if (!is_missing) { + result = version_result; + } }); return result; } else { @@ -203,8 +223,11 @@ template seri::SeriResult DomainGraph::LoadVersion(Version version, T& obj, ObjId obj_id) { auto load = GetReader({obj_id, T::kClassId, V}); - if (load.result != DomainLoadResult::kLoaded) { - return Ok{seri::good}; + if (load.result == DomainLoadResult::kEmpty) { + return Error{domain_internal::missing_version}; + } + if (load.result != DomainLoadResult::kLoaded || load.reader == nullptr) { + return Error{seri::read_error}; } auto bin_archive = @@ -227,9 +250,9 @@ seri::SeriResult DomainGraph::LoadVersion(Version version, T& obj, } template -seri::SeriResult DomainGraph::Save(T const& obj, ObjId obj_id) { +std::optional DomainGraph::Save(T const& obj, ObjId obj_id) { if (!cycle_detector.Add(T::kClassId, obj_id)) { - return Ok{seri::good}; + return std::nullopt; } if constexpr (HasAnyVersionedSave::value) { @@ -290,11 +313,15 @@ struct Serializer, T> { using Archive = BinaryArchive; SeriResult Seri(Archive& arch, Meta meta_val) const { - return arch.buffer().domain_graph->Save(meta_val.value, arch.buffer().id); + auto result = + arch.buffer().domain_graph->Save(meta_val.value, arch.buffer().id); + return result.value_or(Ok{good}); } SeriResult Deseri(Archive& arch, Meta meta_val) const { - return arch.buffer().domain_graph->Load(meta_val.value, arch.buffer().id); + auto result = + arch.buffer().domain_graph->Load(meta_val.value, arch.buffer().id); + return result.value_or(Ok{good}); } }; diff --git a/src/aether-objects/obj/obj_ptr_base.cpp b/src/aether-objects/obj/obj_ptr_base.cpp index 1ceea09..d10a9e0 100644 --- a/src/aether-objects/obj/obj_ptr_base.cpp +++ b/src/aether-objects/obj/obj_ptr_base.cpp @@ -131,7 +131,8 @@ SeriResult ObjPtrBaseSerializer::Seri(Archive& archive, TRY_RESULT((archive.buffer().Write(DataTag{meta.value.id_}))); TRY_RESULT((archive.buffer().Write(DataTag{meta.value.flags_}))); if (meta.value.cached_) { - archive.buffer().domain_graph->SaveRoot(meta.value.cached_, meta.value.id_); + TRY_RESULT(archive.buffer().domain_graph->SaveRoot(meta.value.cached_, + meta.value.id_)); } return Ok{seri::good}; } @@ -145,8 +146,8 @@ SeriResult ObjPtrBaseSerializer::Deseri(Archive& archive, if (meta.value.is_valid() && (meta.value.flags_ & ObjFlags::kUnloadedByDefault) == 0 && (meta.value.flags_ & ObjFlags::kUnloaded) == 0) { - meta.value.cached_ = - archive.buffer().domain_graph->LoadRoot(meta.value.id_); + TRY_RESULT(archive.buffer().domain_graph->LoadRoot(meta.value.id_, + meta.value.cached_)); } return Ok{seri::good}; } diff --git a/src/aether-objects/obj/registrar.h b/src/aether-objects/obj/registrar.h index e1ed48c..86a13c4 100644 --- a/src/aether-objects/obj/registrar.h +++ b/src/aether-objects/obj/registrar.h @@ -63,14 +63,28 @@ class Registrar { } } - static void Load(DomainGraph* domain_graph, Ptr& obj, ObjId obj_id) { + static std::optional Load(DomainGraph* domain_graph, + Ptr& obj, ObjId obj_id) { auto* self_ptr = static_cast(obj.get()); - domain_graph->Load(*self_ptr, obj_id); + auto result = domain_graph->Load(*self_ptr, obj_id); + if (result && result->IsOk()) { + if constexpr (requires { self_ptr->Loaded(); }) { + self_ptr->Loaded(); + } + } + return result; } - static void Save(DomainGraph* domain_graph, Ptr const& obj, ObjId id) { - auto const* self_ptr = static_cast(obj.get()); - domain_graph->Save(*self_ptr, id); + static std::optional Save(DomainGraph* domain_graph, + Ptr const& obj, ObjId id) { + auto* self_ptr = static_cast(obj.get()); + auto result = domain_graph->Save(*self_ptr, id); + if (result && result->IsOk()) { + if constexpr (requires { self_ptr->Saved(); }) { + self_ptr->Saved(); + } + } + return result; } }; } // namespace ae diff --git a/src/aether-objects/obj/registry.h b/src/aether-objects/obj/registry.h index 45de85a..b4af066 100644 --- a/src/aether-objects/obj/registry.h +++ b/src/aether-objects/obj/registry.h @@ -18,6 +18,7 @@ #define AETHER_OBJECTS_OBJ_REGISTRY_H_ #include +#include #include #include #include @@ -32,9 +33,10 @@ class DomainGraph; struct Factory { using CreateFunc = Ptr (*)(); - using LoadFunc = void (*)(DomainGraph* domain_graph, Ptr& obj, ObjId id); - using SaveFunc = void (*)(DomainGraph* domain_graph, Ptr const& obj, - ObjId id); + using LoadFunc = std::optional (*)( + DomainGraph* domain_graph, Ptr& obj, ObjId id); + using SaveFunc = std::optional (*)( + DomainGraph* domain_graph, Ptr const& obj, ObjId id); CreateFunc create; LoadFunc load; From 18083492cd239a7587745f3288a563fb48d957e6 Mon Sep 17 00:00:00 2001 From: BartolomeyKant Date: Fri, 11 Sep 2026 10:53:16 +0500 Subject: [PATCH 4/4] add tests for Loaded and Saved callbacks --- tests/test-object-system/CMakeLists.txt | 1 + tests/test-object-system/main.cpp | 6 +- tests/test-object-system/objects/lifecycle.h | 191 ++++++++++ tests/test-object-system/test-lifecycle.cpp | 341 ++++++++++++++++++ tests/test-object-system/test-obj-create.cpp | 57 +-- .../test-update-objects.cpp | 116 ++++-- 6 files changed, 657 insertions(+), 55 deletions(-) create mode 100644 tests/test-object-system/objects/lifecycle.h create mode 100644 tests/test-object-system/test-lifecycle.cpp diff --git a/tests/test-object-system/CMakeLists.txt b/tests/test-object-system/CMakeLists.txt index 75e67b8..406d163 100644 --- a/tests/test-object-system/CMakeLists.txt +++ b/tests/test-object-system/CMakeLists.txt @@ -18,6 +18,7 @@ cmake_minimum_required( VERSION 3.16 ) list(APPEND test_srcs main.cpp test-env.cpp + test-lifecycle.cpp test-obj-create.cpp test-obj-ptr-forward-decl.cpp test-update-objects.cpp diff --git a/tests/test-object-system/main.cpp b/tests/test-object-system/main.cpp index 4f988c0..a6e21ae 100644 --- a/tests/test-object-system/main.cpp +++ b/tests/test-object-system/main.cpp @@ -21,14 +21,16 @@ void tearDown() {} extern int run_test_object_create(); extern int run_test_env(); -extern int run_test_update_objects(); +extern int test_lifecycle(); +extern int test_update_objects(); extern int run_test_version_iterator(); int main() { int res{}; res += run_test_env(); + res += test_lifecycle(); res += run_test_object_create(); res += run_test_version_iterator(); - res += run_test_update_objects(); + res += test_update_objects(); return res; } diff --git a/tests/test-object-system/objects/lifecycle.h b/tests/test-object-system/objects/lifecycle.h new file mode 100644 index 0000000..a1d538e --- /dev/null +++ b/tests/test-object-system/objects/lifecycle.h @@ -0,0 +1,191 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_OBJECTS_TESTS_TEST_OBJECT_SYSTEM_OBJECTS_LIFECYCLE_H_ +#define AETHER_OBJECTS_TESTS_TEST_OBJECT_SYSTEM_OBJECTS_LIFECYCLE_H_ + +#include + +#include "aether-objects/obj/obj.h" + +namespace ae { +class LifecycleObject : public Obj { + AE_OBJECT(LifecycleObject, Obj, 0) + + LifecycleObject() = default; + + void Loaded() { + ++loaded_count; + runtime_value = persisted_value + 1; + } + + void Saved() { + ++saved_count; + persisted_value = saved_mutation_value; + } + + public: + explicit LifecycleObject(ObjProp prop) : Obj{prop} {} + + AE_OBJECT_REFLECT(AE_MMBR(persisted_value)) + + template + void Load(Version<0>, Dnv& dnv) { + dnv(persisted_value); + } + + static void ResetCallbacks() { + loaded_count = 0; + saved_count = 0; + } + + static inline int loaded_count{}; + static inline int saved_count{}; + static constexpr int saved_mutation_value = 99; + int persisted_value{}; + int runtime_value{}; +}; + +class LifecycleBase : public Obj { + AE_OBJECT(LifecycleBase, Obj, 0) + + public: + void Loaded() { calls.push_back(kBaseLoaded); } + void Saved() { calls.push_back(kBaseSaved); } + + protected: + LifecycleBase() = default; + + public: + explicit LifecycleBase(ObjProp prop) : Obj{prop} {} + + AE_OBJECT_REFLECT() + + static void ResetCalls() { calls.clear(); } + + static constexpr int kBaseLoaded = 1; + static constexpr int kDerivedLoaded = 2; + static constexpr int kBaseSaved = 3; + static constexpr int kDerivedSaved = 4; + static inline std::vector calls; +}; + +class LifecycleDerivedOnly : public LifecycleBase { + AE_OBJECT(LifecycleDerivedOnly, LifecycleBase, 0) + + LifecycleDerivedOnly() = default; + + void Loaded() { calls.push_back(kDerivedLoaded); } + void Saved() { calls.push_back(kDerivedSaved); } + + public: + explicit LifecycleDerivedOnly(ObjProp prop) : LifecycleBase{prop} {} + + AE_OBJECT_REFLECT() +}; + +class LifecycleDerivedChained : public LifecycleBase { + AE_OBJECT(LifecycleDerivedChained, LifecycleBase, 0) + + LifecycleDerivedChained() = default; + + void Loaded() { + LifecycleBase::Loaded(); + calls.push_back(kDerivedLoaded); + } + + void Saved() { + LifecycleBase::Saved(); + calls.push_back(kDerivedSaved); + } + + public: + explicit LifecycleDerivedChained(ObjProp prop) : LifecycleBase{prop} {} + + AE_OBJECT_REFLECT() +}; + +class LifecycleDerivedInherited : public LifecycleBase { + AE_OBJECT(LifecycleDerivedInherited, LifecycleBase, 0) + + LifecycleDerivedInherited() = default; + + public: + explicit LifecycleDerivedInherited(ObjProp prop) : LifecycleBase{prop} {} + + AE_OBJECT_REFLECT() +}; + +class LifecycleGraphObject : public Obj { + AE_OBJECT(LifecycleGraphObject, Obj, 0) + + LifecycleGraphObject() = default; + + void Saved() { + ++saved_count; + saved_ids.push_back(obj_id.id()); + value = saved_mutation_value; + } + + void Loaded() { ++loaded_count; } + + public: + explicit LifecycleGraphObject(ObjProp prop) : Obj{prop} {} + + AE_OBJECT_REFLECT(AE_MMBR(first), AE_MMBR(second), AE_MMBR(value)) + + static void ResetCallbacks() { + loaded_count = 0; + saved_count = 0; + saved_ids.clear(); + } + + static inline int loaded_count{}; + static inline int saved_count{}; + static inline std::vector saved_ids{}; + static constexpr int saved_mutation_value = 99; + ObjPtr first; + ObjPtr second; + int value{}; +}; + +class LifecycleNoexceptObject : public Obj { + AE_OBJECT(LifecycleNoexceptObject, Obj, 0) + + LifecycleNoexceptObject() = default; + + void Loaded() noexcept { ++loaded_count; } + void Loaded(int) {} + void Saved() noexcept { ++saved_count; } + void Saved(int) {} + + public: + explicit LifecycleNoexceptObject(ObjProp prop) : Obj{prop} {} + + AE_OBJECT_REFLECT(AE_MMBR(value)) + + static void ResetCallbacks() { + loaded_count = 0; + saved_count = 0; + } + + static inline int loaded_count{}; + static inline int saved_count{}; + int value{}; +}; +} // namespace ae + +#endif // AETHER_OBJECTS_TESTS_TEST_OBJECT_SYSTEM_OBJECTS_LIFECYCLE_H_ diff --git a/tests/test-object-system/test-lifecycle.cpp b/tests/test-object-system/test-lifecycle.cpp new file mode 100644 index 0000000..fd9c48d --- /dev/null +++ b/tests/test-object-system/test-lifecycle.cpp @@ -0,0 +1,341 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include + +#include "aether-objects/obj/domain.h" +#include "map_domain_storage.h" +#include "objects/lifecycle.h" + +namespace ae::test_lifecycle { +namespace { +inline constexpr char kNestedWriteErrorMessage[] = "Nested write failed"; +inline constexpr seri::SeriError kNestedWriteError{ + .error_code = -1, .message = kNestedWriteErrorMessage}; + +class FailingWriter final : public IDomainStorageWriter { + public: + explicit FailingWriter(seri::SeriError error = seri::write_error) + : error_{error} {} + + seri::SeriResult Write(seri::SizeWriteTag) override { return Error{error_}; } + + seri::SeriResult Write(seri::DataWriteTag) override { return Error{error_}; } + + private: + seri::SeriError error_; +}; + +class FailingSaveStorage final : public MapDomainStorage { + public: + std::unique_ptr Store(DomainQuery const&) override { + return std::make_unique(); + } +}; + +class FailingNestedSaveStorage final : public MapDomainStorage { + public: + std::unique_ptr Store( + DomainQuery const& query) override { + if (query.id == ObjId{2}) { + return std::make_unique(kNestedWriteError); + } + return MapDomainStorage::Store(query); + } +}; + +// Verifies successful saves and loads notify lifecycle callbacks immediately. +void test_SuccessfulCallbacksAreSynchronous() { + auto domain_storage = MapDomainStorage{}; + Domain domain{domain_storage}; + LifecycleObject::ResetCallbacks(); + + auto object = LifecycleObject::ptr::Create(CreateWith{domain}.with_id(1)); + object->persisted_value = 7; + object.Save(); + + TEST_ASSERT_EQUAL(1, LifecycleObject::saved_count); + TEST_ASSERT_EQUAL(LifecycleObject::saved_mutation_value, + object->persisted_value); + + LifecycleObject::ResetCallbacks(); + Domain load_domain{domain_storage}; + auto loaded = + LifecycleObject::ptr::Declare(CreateWith{load_domain}.with_id(1)); + loaded.Load(); + + TEST_ASSERT_EQUAL(1, LifecycleObject::loaded_count); + TEST_ASSERT_EQUAL(7, loaded->persisted_value); + TEST_ASSERT_EQUAL(8, loaded->runtime_value); + loaded.Load(); + TEST_ASSERT_EQUAL(1, LifecycleObject::loaded_count); +} + +// Verifies failed loads, failed saves, and empty pointers do not notify +// callbacks. +void test_FailedOperationsDoNotNotify() { + auto domain_storage = MapDomainStorage{}; + Domain save_domain{domain_storage}; + auto saved = LifecycleObject::ptr::Create(CreateWith{save_domain}.with_id(1)); + saved->persisted_value = 7; + saved.Save(); + domain_storage.map[1][LifecycleObject::kClassId][0]->clear(); + + LifecycleObject::ResetCallbacks(); + Domain load_domain{domain_storage}; + auto loaded = + LifecycleObject::ptr::Declare(CreateWith{load_domain}.with_id(1)); + loaded.Load(); + TEST_ASSERT_EQUAL(0, LifecycleObject::loaded_count); + + auto failing_domain_storage = FailingSaveStorage{}; + Domain failing_save_domain{failing_domain_storage}; + auto to_save = + LifecycleObject::ptr::Create(CreateWith{failing_save_domain}.with_id(2)); + LifecycleObject::ResetCallbacks(); + to_save.Save(); + TEST_ASSERT_EQUAL(0, LifecycleObject::saved_count); + + LifecycleObject::ptr empty; + empty.Save(); + TEST_ASSERT_EQUAL(0, LifecycleObject::saved_count); +} + +// Verifies absent and removed records fail without invoking load callbacks. +void test_AbsentOrRemovedDataDoesNotNotify() { + auto domain_storage = MapDomainStorage{}; + Domain domain{domain_storage}; + + LifecycleObject::ResetCallbacks(); + auto empty = LifecycleObject::ptr::Declare(CreateWith{domain}.with_id(1)); + empty.Load(); + TEST_ASSERT_EQUAL(0, LifecycleObject::loaded_count); + + auto empty_graph = DomainGraph{&domain}; + auto empty_object = + LifecycleObject::ptr::Create(CreateWith{domain}.with_id(3)); + auto empty_result = empty_graph.Load(*empty_object, ObjId{3}); + TEST_ASSERT_TRUE(empty_result.has_value()); + TEST_ASSERT_FALSE(empty_result->IsOk()); + TEST_ASSERT_EQUAL(domain_internal::missing_version.error_code, + empty_result->error().error_code); + + auto saved = LifecycleObject::ptr::Create(CreateWith{domain}.with_id(2)); + saved.Save(); + domain_storage.Remove(ObjId{2}); + + LifecycleObject::ResetCallbacks(); + Domain load_domain{domain_storage}; + auto removed = + LifecycleObject::ptr::Declare(CreateWith{load_domain}.with_id(2)); + removed.Load(); + TEST_ASSERT_EQUAL(0, LifecycleObject::loaded_count); + + auto removed_graph = DomainGraph{&domain}; + auto removed_result = removed_graph.Load(*saved, ObjId{2}); + TEST_ASSERT_TRUE(removed_result.has_value()); + TEST_ASSERT_FALSE(removed_result->IsOk()); + TEST_ASSERT_EQUAL(seri::read_error.error_code, + removed_result->error().error_code); +} + +// Verifies derived callbacks invoke base callbacks only when explicitly +// chained. +void test_DerivedCallbacksRequireExplicitBaseChaining() { + auto domain_storage = MapDomainStorage{}; + Domain domain{domain_storage}; + + LifecycleBase::ResetCalls(); + auto derived_only = + LifecycleDerivedOnly::ptr::Create(CreateWith{domain}.with_id(1)); + derived_only.Save(); + TEST_ASSERT_EQUAL(1, LifecycleBase::calls.size()); + TEST_ASSERT_EQUAL(LifecycleBase::kDerivedSaved, LifecycleBase::calls[0]); + + LifecycleBase::ResetCalls(); + auto derived_chained = + LifecycleDerivedChained::ptr::Create(CreateWith{domain}.with_id(2)); + derived_chained.Save(); + TEST_ASSERT_EQUAL(2, LifecycleBase::calls.size()); + TEST_ASSERT_EQUAL(LifecycleBase::kBaseSaved, LifecycleBase::calls[0]); + TEST_ASSERT_EQUAL(LifecycleBase::kDerivedSaved, LifecycleBase::calls[1]); + + LifecycleBase::ResetCalls(); + Domain load_domain{domain_storage}; + auto loaded_only = + LifecycleDerivedOnly::ptr::Declare(CreateWith{load_domain}.with_id(1)); + loaded_only.Load(); + TEST_ASSERT_EQUAL(1, LifecycleBase::calls.size()); + TEST_ASSERT_EQUAL(LifecycleBase::kDerivedLoaded, LifecycleBase::calls[0]); + + LifecycleBase::ResetCalls(); + auto loaded_chained = + LifecycleDerivedChained::ptr::Declare(CreateWith{load_domain}.with_id(2)); + loaded_chained.Load(); + TEST_ASSERT_EQUAL(2, LifecycleBase::calls.size()); + TEST_ASSERT_EQUAL(LifecycleBase::kBaseLoaded, LifecycleBase::calls[0]); + TEST_ASSERT_EQUAL(LifecycleBase::kDerivedLoaded, LifecycleBase::calls[1]); +} + +// Verifies inherited lifecycle callbacks are invoked for derived objects. +void test_InheritedCallbacksAreNotified() { + auto domain_storage = MapDomainStorage{}; + Domain domain{domain_storage}; + + LifecycleBase::ResetCalls(); + auto derived = + LifecycleDerivedInherited::ptr::Create(CreateWith{domain}.with_id(1)); + derived.Save(); + TEST_ASSERT_EQUAL(1, LifecycleBase::calls.size()); + TEST_ASSERT_EQUAL(LifecycleBase::kBaseSaved, LifecycleBase::calls[0]); + + LifecycleBase::ResetCalls(); + Domain load_domain{domain_storage}; + auto loaded = LifecycleDerivedInherited::ptr::Declare( + CreateWith{load_domain}.with_id(1)); + loaded.Load(); + TEST_ASSERT_EQUAL(1, LifecycleBase::calls.size()); + TEST_ASSERT_EQUAL(LifecycleBase::kBaseLoaded, LifecycleBase::calls[0]); +} + +// Verifies cyclic shared graphs serialize once per object and notify once. +void test_CyclicSharedSerializationReportsSuccessAndNotifiesOnce() { + auto domain_storage = MapDomainStorage{}; + Domain domain{domain_storage}; + LifecycleGraphObject::ResetCallbacks(); + + auto first = LifecycleGraphObject::ptr::Create(CreateWith{domain}.with_id(1)); + auto second = + LifecycleGraphObject::ptr::Create(CreateWith{domain}.with_id(2)); + first->value = 7; + second->value = 8; + first->first = second; + first->second = second; + second->first = first; + auto graph = DomainGraph{&domain}; + Ptr loaded_first = first.Load(); + TEST_ASSERT_TRUE(graph.SaveRoot(loaded_first, ObjId{1}).IsOk()); + + TEST_ASSERT_EQUAL(2, LifecycleGraphObject::saved_count); + TEST_ASSERT_EQUAL( + 1, std::count(LifecycleGraphObject::saved_ids.begin(), + LifecycleGraphObject::saved_ids.end(), ObjId::Type{1})); + TEST_ASSERT_EQUAL( + 1, std::count(LifecycleGraphObject::saved_ids.begin(), + LifecycleGraphObject::saved_ids.end(), ObjId::Type{2})); + + auto repeated_save = graph.Save(*first, ObjId{1}); + TEST_ASSERT_FALSE(repeated_save.has_value()); + TEST_ASSERT_EQUAL(2, LifecycleGraphObject::saved_count); + + Domain load_domain{domain_storage}; + auto loaded = + LifecycleGraphObject::ptr::Declare(CreateWith{load_domain}.with_id(1)); + loaded.Load(); + TEST_ASSERT_EQUAL(7, loaded->value); + TEST_ASSERT_EQUAL(2, LifecycleGraphObject::loaded_count); + + auto repeated_load = graph.Load(*first, ObjId{1}); + TEST_ASSERT_FALSE(repeated_load.has_value()); +} + +// Verifies nested save failures propagate and suppress all save notifications. +void test_NestedSaveFailurePropagatesAndDoesNotNotify() { + auto domain_storage = FailingNestedSaveStorage{}; + Domain domain{domain_storage}; + LifecycleGraphObject::ResetCallbacks(); + + auto first = LifecycleGraphObject::ptr::Create(CreateWith{domain}.with_id(1)); + auto second = + LifecycleGraphObject::ptr::Create(CreateWith{domain}.with_id(2)); + first->first = second; + + auto graph = DomainGraph{&domain}; + Ptr loaded_first = first.Load(); + auto result = graph.SaveRoot(loaded_first, ObjId{1}); + TEST_ASSERT_FALSE(result.IsOk()); + TEST_ASSERT_EQUAL(kNestedWriteError.error_code, result.error().error_code); + TEST_ASSERT_EQUAL_PTR(kNestedWriteError.message.data(), + result.error().message.data()); + TEST_ASSERT_EQUAL(0, LifecycleGraphObject::saved_count); +} + +// Verifies nested load failures propagate and suppress all load notifications. +void test_NestedLoadFailurePropagatesAndDoesNotNotify() { + auto domain_storage = MapDomainStorage{}; + Domain save_domain{domain_storage}; + auto first = + LifecycleGraphObject::ptr::Create(CreateWith{save_domain}.with_id(1)); + auto second = + LifecycleGraphObject::ptr::Create(CreateWith{save_domain}.with_id(2)); + first->first = second; + Ptr loaded_first = first.Load(); + TEST_ASSERT_TRUE( + DomainGraph{&save_domain}.SaveRoot(loaded_first, ObjId{1}).IsOk()); + domain_storage.Remove(ObjId{2}); + + LifecycleGraphObject::ResetCallbacks(); + Domain load_domain{domain_storage}; + Ptr loaded; + auto graph = DomainGraph{&load_domain}; + TEST_ASSERT_FALSE(graph.LoadRoot(ObjId{1}, loaded).IsOk()); + TEST_ASSERT_NULL(loaded.get()); + TEST_ASSERT_EQUAL(0, LifecycleGraphObject::loaded_count); +} + +// Verifies noexcept and overloaded lifecycle callbacks are discovered and +// invoked. +void test_NoexceptAndOverloadedCallbacksAreNotified() { + auto domain_storage = MapDomainStorage{}; + Domain domain{domain_storage}; + LifecycleNoexceptObject::ResetCallbacks(); + + auto object = + LifecycleNoexceptObject::ptr::Create(CreateWith{domain}.with_id(1)); + object->value = 7; + object.Save(); + TEST_ASSERT_EQUAL(1, LifecycleNoexceptObject::saved_count); + + LifecycleNoexceptObject::ResetCallbacks(); + Domain load_domain{domain_storage}; + auto loaded = + LifecycleNoexceptObject::ptr::Declare(CreateWith{load_domain}.with_id(1)); + loaded.Load(); + TEST_ASSERT_EQUAL(1, LifecycleNoexceptObject::loaded_count); +} +} // namespace +} // namespace ae::test_lifecycle + +int test_lifecycle() { + UNITY_BEGIN(); + RUN_TEST(ae::test_lifecycle::test_SuccessfulCallbacksAreSynchronous); + RUN_TEST(ae::test_lifecycle::test_FailedOperationsDoNotNotify); + RUN_TEST(ae::test_lifecycle::test_AbsentOrRemovedDataDoesNotNotify); + RUN_TEST( + ae::test_lifecycle::test_DerivedCallbacksRequireExplicitBaseChaining); + RUN_TEST(ae::test_lifecycle::test_InheritedCallbacksAreNotified); + RUN_TEST(ae::test_lifecycle:: + test_CyclicSharedSerializationReportsSuccessAndNotifiesOnce); + RUN_TEST( + ae::test_lifecycle::test_NestedSaveFailurePropagatesAndDoesNotNotify); + RUN_TEST( + ae::test_lifecycle::test_NestedLoadFailurePropagatesAndDoesNotNotify); + RUN_TEST(ae::test_lifecycle::test_NoexceptAndOverloadedCallbacksAreNotified); + return UNITY_END(); +} diff --git a/tests/test-object-system/test-obj-create.cpp b/tests/test-object-system/test-obj-create.cpp index a103066..f7c8b0e 100644 --- a/tests/test-object-system/test-obj-create.cpp +++ b/tests/test-object-system/test-obj-create.cpp @@ -251,9 +251,9 @@ void TestProxyPtr(Domain& domain) { void test_createFoo() { // create objects - auto facility = MapDomainStorage{}; - Domain domain{facility}; - Domain domain2{facility}; + auto domain_storage = MapDomainStorage{}; + Domain domain{domain_storage}; + Domain domain2{domain_storage}; { Foo::ptr foo = Foo::ptr::Create(CreateWith{domain}.with_id(1)); @@ -264,8 +264,10 @@ void test_createFoo() { }); foo.Save(); - TEST_ASSERT(facility.map.find(foo.id().id()) != facility.map.end()); - TEST_ASSERT(facility.map.find(foo->bar.id().id()) != facility.map.end()); + TEST_ASSERT(domain_storage.map.find(foo.id().id()) != + domain_storage.map.end()); + TEST_ASSERT(domain_storage.map.find(foo->bar.id().id()) != + domain_storage.map.end()); // load object for already loaded list Foo::ptr foo2 = Foo::ptr::Declare(CreateWith{domain}.with_id(1)); @@ -291,8 +293,8 @@ void test_createFoo() { } void test_ObjPtrCachedOwnership() { - auto facility = MapDomainStorage{}; - Domain domain{facility}; + auto domain_storage = MapDomainStorage{}; + Domain domain{domain_storage}; test_obj_create_internal::TestEmptyAndUnloadedPtrOwnership(domain); test_obj_create_internal::TestCachedPtrOwnership(domain); @@ -300,16 +302,16 @@ void test_ObjPtrCachedOwnership() { } void test_ObjPtrTypedViewOwnership() { - auto facility = MapDomainStorage{}; - Domain domain{facility}; + auto domain_storage = MapDomainStorage{}; + Domain domain{domain_storage}; test_obj_create_internal::TestEmptyAndUnloadedTypedViewOwnership(domain); test_obj_create_internal::TestLoadedTypedViewOwnership(domain); } void test_createBob() { - auto facility = MapDomainStorage{}; - Domain domain{facility}; + auto domain_storage = MapDomainStorage{}; + Domain domain{domain_storage}; { Bob::ptr bob = Bob::ptr::Create(CreateWith{domain}.with_id(1)); @@ -341,8 +343,8 @@ void test_createBob() { } void test_cloneFoo() { - auto facility = MapDomainStorage{}; - Domain domain{facility}; + auto domain_storage = MapDomainStorage{}; + Domain domain{domain_storage}; auto foo_prefab = Foo::ptr::Create(CreateWith{domain}.with_id(100)); TEST_ASSERT(foo_prefab); foo_prefab.Save(); @@ -363,8 +365,8 @@ void test_cloneFoo() { } void test_createBobsMother() { - auto facility = MapDomainStorage{}; - Domain domain{facility}; + auto domain_storage = MapDomainStorage{}; + Domain domain{domain_storage}; { BobsMother::ptr bobs_mother = @@ -387,8 +389,8 @@ void test_createBobsMother() { } void test_createBobsFather() { - auto facility = MapDomainStorage{}; - Domain domain{facility}; + auto domain_storage = MapDomainStorage{}; + Domain domain{domain_storage}; { BobsFather::ptr bobs_father = @@ -414,8 +416,8 @@ void test_createBobsFather() { } void test_createCollector() { - auto facility = MapDomainStorage{}; - Domain domain{facility}; + auto domain_storage = MapDomainStorage{}; + Domain domain{domain_storage}; { Collector::ptr collector = Collector::ptr::Create(CreateWith{domain}.with_id(1)); @@ -446,8 +448,8 @@ void test_createCollector() { } void test_cyclePoopaLoopa() { - auto facility = MapDomainStorage{}; - Domain domain{facility}; + auto domain_storage = MapDomainStorage{}; + Domain domain{domain_storage}; Poopa::DeleteCount = 0; Loopa::DeleteCount = 0; @@ -486,8 +488,8 @@ void test_cyclePoopaLoopa() { } void test_cyclePoopaLoopaReverse() { - auto facility = MapDomainStorage{}; - Domain domain{facility}; + auto domain_storage = MapDomainStorage{}; + Domain domain{domain_storage}; Poopa::DeleteCount = 0; Loopa::DeleteCount = 0; @@ -521,16 +523,17 @@ void test_cyclePoopaLoopaReverse() { } void test_Family() { - auto facility = MapDomainStorage{}; - Domain domain{facility}; + auto domain_storage = MapDomainStorage{}; + Domain domain{domain_storage}; // create child and test is father and obj saved too { Child::ptr child = Child::ptr::Create(CreateWith{domain}.with_id(1)); TEST_ASSERT(child); child.Save(); - TEST_ASSERT(facility.map.find(child.id().id()) != facility.map.end()); + TEST_ASSERT(domain_storage.map.find(child.id().id()) != + domain_storage.map.end()); - auto& classes = facility.map[child.id().id()]; + auto& classes = domain_storage.map[child.id().id()]; TEST_ASSERT(classes.find(Child::kClassId) != classes.end()); TEST_ASSERT_EQUAL(0, classes[Child::kClassId][0]->size()); TEST_ASSERT(classes.find(Father::kClassId) != classes.end()); diff --git a/tests/test-object-system/test-update-objects.cpp b/tests/test-object-system/test-update-objects.cpp index 73f0f59..c4bd9c2 100644 --- a/tests/test-object-system/test-update-objects.cpp +++ b/tests/test-object-system/test-update-objects.cpp @@ -21,10 +21,12 @@ #include "objects/seven_fridays.h" namespace ae::test_update_objects { -// replace class id in map facility to imitate one class save and load -void replace_class_id(MapDomainStorage& facility, std::uint32_t old_class, +constexpr float kStoredValue = 42.42F; + +// replace class id in map domain storage to imitate one class save and load +void replace_class_id(MapDomainStorage& domain_storage, std::uint32_t old_class, std::uint32_t new_class) { - for (auto& [_, classes] : facility.map) { + for (auto& [_, classes] : domain_storage.map) { auto it = classes.find(old_class); if (it != classes.end()) { classes[new_class] = std::move(it->second); @@ -33,17 +35,18 @@ void replace_class_id(MapDomainStorage& facility, std::uint32_t old_class, } } -// remove class id from map facility to imitate one class was removed from +// remove class id from map domain storage to imitate one class was removed from // inheritance chain -void remove_class_id(MapDomainStorage& facility, std::uint32_t class_id) { - for (auto& [_, classes] : facility.map) { +void remove_class_id(MapDomainStorage& domain_storage, std::uint32_t class_id) { + for (auto& [_, classes] : domain_storage.map) { classes.erase(class_id); } } -void test_increaseVersion() { - auto facility = MapDomainStorage{}; - Domain domain{facility}; +// Verifies stored objects can be loaded and saved through successive upgrades. +void test_IncreaseVersion() { + auto domain_storage = MapDomainStorage{}; + Domain domain{domain_storage}; // the oldest version { Friday0::ptr friday = Friday0::ptr::Create(CreateWith{domain}.with_id(1)); @@ -51,7 +54,7 @@ void test_increaseVersion() { } // load version 1 from version 0 - replace_class_id(facility, Friday0::kClassId, Friday1::kClassId); + replace_class_id(domain_storage, Friday0::kClassId, Friday1::kClassId); { Friday1::ptr friday = Friday1::ptr::Declare(CreateWith{domain}.with_id(1)); friday.WithLoaded([](auto const& p) { @@ -65,32 +68,34 @@ void test_increaseVersion() { } // load version 2 from version 1 - replace_class_id(facility, Friday1::kClassId, Friday2::kClassId); + replace_class_id(domain_storage, Friday1::kClassId, Friday2::kClassId); { Friday2::ptr friday = Friday2::ptr::Declare(CreateWith{domain}.with_id(1)); friday.WithLoaded([](auto const& p) { TEST_ASSERT(p); - TEST_ASSERT_EQUAL_FLOAT(123.f, p->a); - p->a = 42.42f; + TEST_ASSERT_EQUAL_FLOAT(123.0F, p->a); + p->a = kStoredValue; }); friday.Save(); } // load version 3 from version 2 - replace_class_id(facility, Friday2::kClassId, Friday3::kClassId); + replace_class_id(domain_storage, Friday2::kClassId, Friday3::kClassId); { Friday3::ptr friday = Friday3::ptr::Declare(CreateWith{domain}.with_id(1)); friday.WithLoaded([](auto const& p) { TEST_ASSERT(p); - TEST_ASSERT_EQUAL(42.42f, p->a); + TEST_ASSERT_EQUAL_FLOAT(kStoredValue, p->a); TEST_ASSERT_EQUAL_STRING("", p->x.c_str()); }); } } -void test_decreaseVersion() { - auto facility = MapDomainStorage{}; - Domain domain{facility}; +// Verifies stored objects can be loaded and saved through successive +// downgrades. +void test_DecreaseVersion() { + auto domain_storage = MapDomainStorage{}; + Domain domain{domain_storage}; // the newest version { Friday3::ptr friday = Friday3::ptr::Create(CreateWith{domain}.with_id(1)); @@ -102,21 +107,21 @@ void test_decreaseVersion() { } // load version 2 from version 3 - replace_class_id(facility, Friday3::kClassId, Friday2::kClassId); + replace_class_id(domain_storage, Friday3::kClassId, Friday2::kClassId); // it's not allowed to remove class from inheritance chain and leave it in // registry - remove_class_id(facility, Hoopa::kClassId); + remove_class_id(domain_storage, Hoopa::kClassId); { Friday2::ptr friday = Friday2::ptr::Declare(CreateWith{domain}.with_id(1)); friday.WithLoaded([](auto const& f) { TEST_ASSERT(f); - TEST_ASSERT_EQUAL(123.123f, f->a); + TEST_ASSERT_EQUAL_FLOAT(123.123F, f->a); }); friday.Save(); } // load version 1 from version 2 - replace_class_id(facility, Friday2::kClassId, Friday1::kClassId); + replace_class_id(domain_storage, Friday2::kClassId, Friday1::kClassId); { Friday1::ptr friday = Friday1::ptr::Declare(CreateWith{domain}.with_id(1)); friday.WithLoaded([](auto const& f) { @@ -129,17 +134,76 @@ void test_decreaseVersion() { } // load version 0 from version 1 - replace_class_id(facility, Friday1::kClassId, Friday0::kClassId); + replace_class_id(domain_storage, Friday1::kClassId, Friday0::kClassId); { Friday0::ptr friday = Friday0::ptr::Declare(CreateWith{domain}.with_id(1)); friday.WithLoaded([](auto const& f) { TEST_ASSERT(f); }); } } + +// Verifies a missing newest record falls back to an older supported version. +void test_MissingVersionFallsBackToSupportedVersion() { + auto domain_storage = MapDomainStorage{}; + Domain domain{domain_storage}; + auto saved = Friday2::ptr::Create(CreateWith{domain}.with_id(1)); + saved->a = kStoredValue; + saved.Save(); + + // Remove the newest version record while retaining the older compatible one. + domain_storage.map[1][Friday2::kClassId].erase(0); + auto graph = DomainGraph{&domain}; + auto result = graph.Load(*saved, ObjId{1}); + + TEST_ASSERT_TRUE(result.has_value()); + TEST_ASSERT_TRUE(result->IsOk()); + TEST_ASSERT_EQUAL_FLOAT(kStoredValue, saved->a); +} + +// Verifies loading reports missing_version when no serialized version exists. +void test_AllMissingVersionsReturnMissingVersionError() { + auto domain_storage = MapDomainStorage{}; + Domain domain{domain_storage}; + auto object = Friday2::ptr::Create(CreateWith{domain}.with_id(1)); + auto graph = DomainGraph{&domain}; + auto result = graph.Load(*object, ObjId{1}); + + TEST_ASSERT_TRUE(result.has_value()); + TEST_ASSERT_FALSE(result->IsOk()); + TEST_ASSERT_EQUAL(domain_internal::missing_version.error_code, + result->error().error_code); +} + +// Verifies a malformed present version reports its error without fallback. +void test_RealVersionErrorStopsIteration() { + auto domain_storage = MapDomainStorage{}; + Domain domain{domain_storage}; + auto saved = Friday2::ptr::Create(CreateWith{domain}.with_id(1)); + saved->a = kStoredValue; + saved.Save(); + + // Removing a version is recoverable, but corrupting a present record is not. + // Version 0 has no serialized fields, whereas version 2 stores `a`. + // Clearing version 2 therefore produces read_eof instead of a fallback. + auto& version_data = domain_storage.map[1][Friday2::kClassId][2]; + TEST_ASSERT_TRUE(version_data.has_value()); + version_data.value().clear(); + auto graph = DomainGraph{&domain}; + auto result = graph.Load(*saved, ObjId{1}); + + TEST_ASSERT_TRUE(result.has_value()); + TEST_ASSERT_FALSE(result->IsOk()); + TEST_ASSERT_EQUAL(seri::read_eof.error_code, result->error().error_code); +} } // namespace ae::test_update_objects -int run_test_update_objects() { +int test_update_objects() { UNITY_BEGIN(); - RUN_TEST(ae::test_update_objects::test_increaseVersion); - RUN_TEST(ae::test_update_objects::test_decreaseVersion); + RUN_TEST(ae::test_update_objects::test_IncreaseVersion); + RUN_TEST(ae::test_update_objects::test_DecreaseVersion); + RUN_TEST( + ae::test_update_objects::test_MissingVersionFallsBackToSupportedVersion); + RUN_TEST(ae::test_update_objects:: + test_AllMissingVersionsReturnMissingVersionError); + RUN_TEST(ae::test_update_objects::test_RealVersionErrorStopsIteration); return UNITY_END(); }