diff --git a/score/kvs/docs/requirements/index.rst b/score/kvs/docs/requirements/index.rst index 4c6042c6..b56b93d2 100644 --- a/score/kvs/docs/requirements/index.rst +++ b/score/kvs/docs/requirements/index.rst @@ -302,6 +302,17 @@ Component Requirements The component shall create a snapshot each time data is stored. +.. comp_req:: Snapshot Explicit Creation + :id: comp_req__kvs__snapshot_explicit_creation + :reqtype: Functional + :security: NO + :safety: ASIL_B + :satisfies: feat_req__persistency__snapshot_create + :status: valid + :belongs_to: comp__persistency_kvs + + The component shall create a snapshot in the first available slot when the snapshot_create function is explicitly called. + .. comp_req:: Snapshot Maximum Number :id: comp_req__kvs__snapshot_max_num :reqtype: Functional diff --git a/src/cpp/src/kvs.cpp b/src/cpp/src/kvs.cpp index 1fc39005..fcb705bc 100644 --- a/src/cpp/src/kvs.cpp +++ b/src/cpp/src/kvs.cpp @@ -593,19 +593,11 @@ score::ResultBlank Kvs::flush() result = score::MakeUnexpected(ErrorCode::JsonGeneratorError); } else - { - /* Rotate Snapshots */ - auto rotate_result = snapshot_rotate(); - if (!rotate_result) - { - result = rotate_result; - } - else - { - /* Write JSON Data */ - std::string buf = std::move(buf_res.value()); - result = write_json_data(buf); - } + { + /* Write JSON Data */ + std::string buf = std::move(buf_res.value()); + result = write_json_data(buf); + } } @@ -613,99 +605,129 @@ score::ResultBlank Kvs::flush() } /* Retrieve the snapshot count*/ -score::Result Kvs::snapshot_count() const +score::Result Kvs::snapshot_count() const { - score::Result result = score::MakeUnexpected(ErrorCode::UnmappedError); size_t count = 0; - bool error = false; - for (size_t idx = 0; idx < KVS_MAX_SNAPSHOTS; ++idx) + for (size_t idx = 1; idx <= KVS_MAX_SNAPSHOTS; ++idx) { const score::filesystem::Path fname = filename_prefix.Native() + "_" + to_string(idx) + ".json"; const auto fname_exists_res = filesystem->standard->Exists(fname); - if (fname_exists_res) + if (!fname_exists_res) { - if (false == fname_exists_res.value()) - { - break; - } + /* Filesystem error: cannot determine whether the slot exists */ + return score::MakeUnexpected(ErrorCode::PhysicalStorageFailure); } - else + if (fname_exists_res.value()) { - error = true; - break; + count++; } - count = idx + 1; + /* Slot is empty: do nothing and continue to the next slot */ } - if (error) + return count; +} + +score::Result Kvs::snapshot_create() +{ + score::Result result = score::MakeUnexpected(ErrorCode::UnmappedError); + std::unique_lock lock(kvs_mutex, std::try_to_lock); + if (lock.owns_lock()) { - result = score::MakeUnexpected(ErrorCode::PhysicalStorageFailure); + auto snapshot_count_res = snapshot_count(); + if (!snapshot_count_res) + { + result = score::MakeUnexpected(static_cast(*snapshot_count_res.error())); + } + else + { + if (snapshot_count_res.value() >= snapshot_max_count()) + { + result = score::MakeUnexpected(ErrorCode::QuotaExceeded); + } + else + { + auto first_free_slot_res = first_free_slot(); + if (!first_free_slot_res) + { + result = score::MakeUnexpected(static_cast(*first_free_slot_res.error())); + } + else + { + const size_t new_snapshot_id = first_free_slot_res.value(); + const score::filesystem::Path src_json{filename_prefix.Native() + "_0.json"}; + const score::filesystem::Path src_hash{filename_prefix.Native() + "_0.hash"}; + const score::filesystem::Path dst_json{filename_prefix.Native() + "_" + to_string(new_snapshot_id) + ".json"}; + const score::filesystem::Path dst_hash{filename_prefix.Native() + "_" + to_string(new_snapshot_id) + ".hash"}; + + const auto copy_json_res = filesystem->standard->CopyFile(src_json, dst_json); + if (!copy_json_res) + { + logger->LogError() << "Failed to copy snapshot JSON file to '" << dst_json << "'"; + result = score::MakeUnexpected(ErrorCode::PhysicalStorageFailure); + } + else + { + const auto copy_hash_res = filesystem->standard->CopyFile(src_hash, dst_hash); + if (!copy_hash_res) + { + logger->LogError() << "Failed to copy snapshot hash file to '" << dst_hash << "'"; + result = score::MakeUnexpected(ErrorCode::PhysicalStorageFailure); + } + else + { + result = new_snapshot_id; + } + } + } + } + } } else { - result = count; + result = score::MakeUnexpected(ErrorCode::MutexLockFailed); } return result; } /* Retrieve the max snapshot count*/ -size_t Kvs::snapshot_max_count() const +std::size_t Kvs::snapshot_max_count() const { return KVS_MAX_SNAPSHOTS; } -/* Rotate Snapshots */ -score::ResultBlank Kvs::snapshot_rotate() +/* Restore the key-value store from a snapshot*/ +score::ResultBlank Kvs::snapshot_restore(const SnapshotId& snapshot_id) { score::ResultBlank result = score::MakeUnexpected(ErrorCode::UnmappedError); std::unique_lock lock(kvs_mutex, std::try_to_lock); if (lock.owns_lock()) { - bool error = false; - for (size_t idx = KVS_MAX_SNAPSHOTS; idx > 0; --idx) + auto snapshot_count_res = snapshot_count(); + if (!snapshot_count_res) { - score::filesystem::Path hash_old = filename_prefix.Native() + "_" + to_string(idx - 1) + ".hash"; - score::filesystem::Path hash_new = filename_prefix.Native() + "_" + to_string(idx) + ".hash"; - score::filesystem::Path snap_old = filename_prefix.Native() + "_" + to_string(idx - 1) + ".json"; - score::filesystem::Path snap_new = filename_prefix.Native() + "_" + to_string(idx) + ".json"; - - logger->LogInfo() << "rotating: " << snap_old << " -> " << snap_new; - /* Rename hash */ - int32_t hash_rename = std::rename(hash_old.CStr(), hash_new.CStr()); - if (0 != hash_rename) + result = score::MakeUnexpected(static_cast(*snapshot_count_res.error())); + } + else + { + if (snapshot_count_res.value() == 0 || snapshot_id.id >= snapshot_max_count()) { - if (errno != ENOENT) - { - error = true; - logger->LogError() << "error: could not rename hash file " << snap_old << ". Rename Errorcode " - << errno; - result = score::MakeUnexpected(ErrorCode::PhysicalStorageFailure); - } + result = score::MakeUnexpected(ErrorCode::InvalidSnapshotId); } - if (!error) + else { - /* Rename snapshot */ - int32_t snap_rename = std::rename(snap_old.CStr(), snap_new.CStr()); - if (0 != snap_rename) + score::filesystem::Path restore_path = filename_prefix.Native() + "_" + to_string(snapshot_id.id + 1); + auto data_res = open_json(restore_path, OpenJsonNeedFile::Required); + if (!data_res) { - if (errno != ENOENT) - { - error = true; - logger->LogError() - << "error: could not rename snapshot file " << snap_old << ". Rename Errorcode " << errno; - result = score::MakeUnexpected(ErrorCode::PhysicalStorageFailure); - } + result = score::MakeUnexpected(static_cast(*data_res.error())); + } + else + { + kvs = std::move(data_res.value()); + result = score::ResultBlank{}; } - } - if (error) - { - break; } } - if (!error) - { - result = score::ResultBlank{}; - } } else { @@ -715,41 +737,61 @@ score::ResultBlank Kvs::snapshot_rotate() return result; } -/* Restore the key-value store from a snapshot*/ -score::ResultBlank Kvs::snapshot_restore(const SnapshotId& snapshot_id) +/* Delete a snapshot*/ +score::ResultBlank Kvs::snapshot_delete(const SnapshotId& snapshot_id) { score::ResultBlank result = score::MakeUnexpected(ErrorCode::UnmappedError); std::unique_lock lock(kvs_mutex, std::try_to_lock); if (lock.owns_lock()) - { + { auto snapshot_count_res = snapshot_count(); if (!snapshot_count_res) { - result = score::MakeUnexpected(static_cast(*snapshot_count_res.error())); + return score::MakeUnexpected(static_cast(*snapshot_count_res.error())); + } + + const std::size_t count = snapshot_count_res.value(); + if (count == 0 || snapshot_id.id >= snapshot_max_count()) + { + result = score::MakeUnexpected(ErrorCode::InvalidSnapshotId); } else { - /* Fail if the snapshot ID is the current KVS */ - if (0 == snapshot_id.id) + /* Delete the target snapshot files (snapshot files are 1-indexed: _1.json, _2.json, ...) */ + const score::filesystem::Path json_path{filename_prefix.Native() + "_" + to_string(snapshot_id.id + 1) + ".json"}; + const score::filesystem::Path hash_path{filename_prefix.Native() + "_" + to_string(snapshot_id.id + 1) + ".hash"}; + + const auto json_exists_res = filesystem->standard->Exists(json_path); + if (!json_exists_res) { - result = score::MakeUnexpected(ErrorCode::InvalidSnapshotId); + logger->LogError() << "Failed to check existence of snapshot JSON file '" << json_path << "'"; + result = score::MakeUnexpected(ErrorCode::PhysicalStorageFailure); } - else if (snapshot_count_res.value() < snapshot_id.id) + else if (!json_exists_res.value()) { + logger->LogError() << "Snapshot JSON file does not exist: '" << json_path << "'"; result = score::MakeUnexpected(ErrorCode::InvalidSnapshotId); } else { - score::filesystem::Path restore_path = filename_prefix.Native() + "_" + to_string(snapshot_id.id); - auto data_res = open_json(restore_path, OpenJsonNeedFile::Required); - if (!data_res) + const auto remove_json_res = filesystem->standard->Remove(json_path); + if (!remove_json_res) { - result = score::MakeUnexpected(static_cast(*data_res.error())); + logger->LogError() << "Failed to delete snapshot JSON file '" << json_path << "'"; + result = score::MakeUnexpected(ErrorCode::PhysicalStorageFailure); } else { - kvs = std::move(data_res.value()); - result = score::ResultBlank{}; + const auto remove_hash_res = filesystem->standard->Remove(hash_path); + if (!remove_hash_res) + { + logger->LogError() << "Failed to delete snapshot hash file '" << hash_path << "'"; + result = score::MakeUnexpected(ErrorCode::PhysicalStorageFailure); + } + else + { + result = score::ResultBlank{}; + } } } } @@ -762,6 +804,27 @@ score::ResultBlank Kvs::snapshot_restore(const SnapshotId& snapshot_id) return result; } +/* Returns the index (1-based) of the first snapshot slot that is free. + Checks _1, _2, _3 (up to KVS_MAX_SNAPSHOTS) and returns the first index + whose .json file does not exist. Returns OutOfStorageSpace if all slots are occupied. */ +score::Result Kvs::first_free_slot() const +{ + for (size_t idx = 1U; idx <= KVS_MAX_SNAPSHOTS; ++idx) + { + const score::filesystem::Path fname = filename_prefix.Native() + "_" + to_string(idx) + ".json"; + const auto exists_res = filesystem->standard->Exists(fname); + if (!exists_res) + { + return score::MakeUnexpected(ErrorCode::PhysicalStorageFailure); + } + if (!exists_res.value()) + { + return idx; /* First free slot found */ + } + } + return score::MakeUnexpected(ErrorCode::OutOfStorageSpace); +} + /* Get the filename for a snapshot*/ score::Result Kvs::get_kvs_filename(const SnapshotId& snapshot_id) const { diff --git a/src/cpp/src/kvs.hpp b/src/cpp/src/kvs.hpp index cdccf1a0..8a1662cc 100644 --- a/src/cpp/src/kvs.hpp +++ b/src/cpp/src/kvs.hpp @@ -300,7 +300,7 @@ class Kvs final * - On success: The total count of snapshots as a size_t value. * - On failure: Returns an ErrorCode describing the error. */ - score::Result snapshot_count() const; + score::Result snapshot_count() const; /** * @brief Retrieves the maximum number of snapshots that can be stored. @@ -308,9 +308,36 @@ class Kvs final * This function returns the upper limit on the number of snapshots * that the key-value store can maintain at any given time. * - * @return The maximum count of snapshots as a size_t value. + * @return The maximum count of snapshots as a std::size_t value. */ - size_t snapshot_max_count() const; + std::size_t snapshot_max_count() const; + + /** + * @brief Creates a new snapshot of the current state of the key-value store. + * + * This function captures the current state of the key-value store and saves it + * as a snapshot. The snapshot can later be used to restore the key-value store + * to this exact state using the snapshot_restore function. + * + * @return score::ResultBlank + * - On success: The identifier of the created snapshot as a std::size_t value. + * - On failure: An error code describing the reason for the failure. + */ + score::Result snapshot_create(); + + /** + * @brief Deletes a specified snapshot from the key-value store. + * + * This function removes the snapshot identified by the given snapshot ID + * from the key-value store. Once deleted, the snapshot can no longer be + * used for restoration purposes. This operation is irreversible. + * + * @param snapshot_id The identifier of the snapshot to be deleted. + * @return score::ResultBlank + * - On success: An empty score::Result indicating the snapshot was deleted successfully. + * - On failure: An error code describing the reason for the failure. + */ + score::ResultBlank snapshot_delete(const SnapshotId& snapshot_id); /** * @brief Restores the state of the key-value store from a specified snapshot. @@ -377,7 +404,7 @@ class Kvs final std::unique_ptr logger; /* Private Methods */ - score::ResultBlank snapshot_rotate(); + score::Result first_free_slot() const; score::Result> parse_json_data(const std::string& data); score::Result> open_json(const score::filesystem::Path& prefix, OpenJsonNeedFile need_file); diff --git a/src/cpp/tests/test_kvs.cpp b/src/cpp/tests/test_kvs.cpp index c3d5d080..fcbc8ebf 100644 --- a/src/cpp/tests/test_kvs.cpp +++ b/src/cpp/tests/test_kvs.cpp @@ -719,292 +719,304 @@ TEST(kvs_write_json_data, write_json_data_permissions_failure) cleanup_environment(); } -TEST(kvs_snapshot_rotate, snapshot_rotate_success) +TEST(kvs_flush, flush_success_data) { prepare_environment(); + /* Test flush with valid data */ + system(("rm -rf " + kvs_prefix + ".json").c_str()); + system(("rm -rf " + kvs_prefix + ".hash").c_str()); auto result = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); ASSERT_TRUE(result); - /* Create empty Test-Snapshot Files */ - for (size_t i = 0; i < KVS_MAX_SNAPSHOTS; i++) - { - std::ofstream(filename_prefix + "_" + std::to_string(i) + ".json") << "{}"; - std::ofstream(filename_prefix + "_" + std::to_string(i) + ".hash") << "{}"; - EXPECT_EQ(result.value().snapshot_count().value(), i+1); - } - ASSERT_FALSE(std::filesystem::exists(filename_prefix + "_" + std::to_string(KVS_MAX_SNAPSHOTS) + ".json")); - ASSERT_FALSE(std::filesystem::exists(filename_prefix + "_" + std::to_string(KVS_MAX_SNAPSHOTS) + ".hash")); - - /* Rotate Snapshots */ - auto rotate_result = result.value().snapshot_rotate(); - ASSERT_TRUE(rotate_result); + result.value().kvs.clear(); /* Clear KVS to ensure no data is written */ + std::string value = "value1"; + result.value().kvs.insert({"key1", KvsValue(value)}); + auto flush_result = result.value().flush(); + ASSERT_TRUE(flush_result); - /* Check if the snapshot ids are rotated and no ID 0 exists */ - EXPECT_TRUE(std::filesystem::exists(filename_prefix + "_" + std::to_string(KVS_MAX_SNAPSHOTS) + ".json")); - EXPECT_TRUE(std::filesystem::exists(filename_prefix + "_" + std::to_string(KVS_MAX_SNAPSHOTS) + ".hash")); - EXPECT_FALSE(std::filesystem::exists(filename_prefix + "_" + std::to_string(0) + ".json")); - EXPECT_FALSE(std::filesystem::exists(filename_prefix + "_" + std::to_string(0) + ".hash")); + /* Check if files were created correctly */ + EXPECT_TRUE(std::filesystem::exists(kvs_prefix + ".json")); + EXPECT_TRUE(std::filesystem::exists(kvs_prefix + ".hash")); + EXPECT_FALSE(std::filesystem::exists(filename_prefix + "_1.json")); + EXPECT_FALSE(std::filesystem::exists(filename_prefix + "_1.hash")); cleanup_environment(); } -TEST(kvs_snapshot_rotate, snapshot_rotate_max_snapshots) +TEST(kvs_flush, flush_failure_mutex) { prepare_environment(); auto result = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); ASSERT_TRUE(result); - /* Create empty Test-Snapshot Files */ - for (size_t i = 0; i < KVS_MAX_SNAPSHOTS; i++) - { - std::ofstream(filename_prefix + "_" + std::to_string(i) + ".json") << "{}"; - std::ofstream(filename_prefix + "_" + std::to_string(i) + ".hash") << "{}"; - EXPECT_EQ(result.value().snapshot_count().value(), i+1); - } - ASSERT_FALSE(std::filesystem::exists(filename_prefix + "_" + std::to_string(KVS_MAX_SNAPSHOTS) + ".json")); - ASSERT_FALSE(std::filesystem::exists(filename_prefix + "_" + std::to_string(KVS_MAX_SNAPSHOTS) + ".hash")); - - /* Check if no ID higher than KVS_MAX_SNAPSHOTS exists */ - auto rotate_result = result.value().snapshot_rotate(); - ASSERT_TRUE(rotate_result); - EXPECT_FALSE(std::filesystem::exists(filename_prefix + "_" + std::to_string(KVS_MAX_SNAPSHOTS + 1) + ".json")); - EXPECT_FALSE(std::filesystem::exists(filename_prefix + "_" + std::to_string(KVS_MAX_SNAPSHOTS + 1) + ".hash")); + std::unique_lock lock(result.value().kvs_mutex); + auto flush_result = result.value().flush(); + EXPECT_FALSE(flush_result); + EXPECT_EQ(static_cast(*flush_result.error()), ErrorCode::MutexLockFailed); cleanup_environment(); } -TEST(kvs_snapshot_rotate, snapshot_rotate_failure_renaming_json) +TEST(kvs_flush, flush_failure_kvsvalue_invalid) { prepare_environment(); auto result = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); ASSERT_TRUE(result); - /* Create empty Test-Snapshot Files */ - for (size_t i = 0; i < KVS_MAX_SNAPSHOTS; i++) - { - std::ofstream(filename_prefix + "_" + std::to_string(i) + ".json") << "{}"; - std::ofstream(filename_prefix + "_" + std::to_string(i) + ".hash") << "{}"; - EXPECT_EQ(result.value().snapshot_count().value(), i+1); - } + BrokenKvsValue invalid; + result.value().kvs.insert({"invalid_key", invalid}); - /* Snapshot (JSON) Renaming failed (Create directorys instead of json files to trigger rename - * error)*/ - std::filesystem::create_directory(filename_prefix + "_" + std::to_string(KVS_MAX_SNAPSHOTS) + ".json"); - auto rotate_result = result.value().snapshot_rotate(); - EXPECT_FALSE(rotate_result); - EXPECT_EQ(static_cast(*rotate_result.error()), ErrorCode::PhysicalStorageFailure); + auto flush_result_invalid = result.value().flush(); + EXPECT_FALSE(flush_result_invalid); + EXPECT_EQ(flush_result_invalid.error(), ErrorCode::InvalidValueType); cleanup_environment(); } -TEST(kvs_snapshot_rotate, snapshot_rotate_failure_renaming_hash) +TEST(kvs_flush, flush_failure_json_writer) { prepare_environment(); - auto result = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); - ASSERT_TRUE(result); + auto kvs = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); + ASSERT_TRUE(kvs); - /* Create empty Test-Snapshot Files */ - for (size_t i = 0; i < KVS_MAX_SNAPSHOTS; i++) - { - std::ofstream(filename_prefix + "_" + std::to_string(i) + ".json") << "{}"; - std::ofstream(filename_prefix + "_" + std::to_string(i) + ".hash") << "{}"; - EXPECT_EQ(result.value().snapshot_count().value(), i+1); - } + auto mock_writer = std::make_unique(); /* Force error in writer.ToBuffer */ + EXPECT_CALL(*mock_writer, ToBuffer(::testing::A())) + .WillOnce( + ::testing::Return(score::Result(score::MakeUnexpected(score::json::Error::kUnknownError)))); - /* Hash Renaming failed (Create directorys instead of json files to trigger rename error)*/ - std::filesystem::create_directory(filename_prefix + "_" + std::to_string(KVS_MAX_SNAPSHOTS) + ".hash"); - auto rotate_result = result.value().snapshot_rotate(); - EXPECT_FALSE(rotate_result); - EXPECT_EQ(static_cast(*rotate_result.error()), ErrorCode::PhysicalStorageFailure); + kvs->writer = std::move(mock_writer); + auto result = kvs.value().flush(); + EXPECT_FALSE(result); + EXPECT_EQ(result.error(), ErrorCode::JsonGeneratorError); cleanup_environment(); } -TEST(kvs_snapshot_rotate, snapshot_rotate_failure_mutex) +TEST(kvs_snapshot_count, snapshot_count_success) { prepare_environment(); auto result = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); ASSERT_TRUE(result); - /* Mutex locked */ - std::unique_lock lock(result.value().kvs_mutex); - auto rotate_result = result.value().snapshot_rotate(); - EXPECT_FALSE(rotate_result); - EXPECT_EQ(static_cast(*rotate_result.error()), ErrorCode::MutexLockFailed); + /* No snapshot files exist yet: count must be 0 */ + { + auto count = result.value().snapshot_count(); + EXPECT_TRUE(count); + EXPECT_EQ(count.value(), 0U); + } + + + /* Crea snapshot non contigui e verifica che il conteggio sia corretto */ + // Solo _2.json + std::ofstream(filename_prefix + "_2.json") << "{}"; + auto count = result.value().snapshot_count(); + EXPECT_TRUE(count); + EXPECT_EQ(count.value(), 1U); + + // _2.json e _3.json + std::ofstream(filename_prefix + "_3.json") << "{}"; + count = result.value().snapshot_count(); + EXPECT_TRUE(count); + EXPECT_EQ(count.value(), 2U); + + // _1.json, _2.json, _3.json + std::ofstream(filename_prefix + "_1.json") << "{}"; + count = result.value().snapshot_count(); + EXPECT_TRUE(count); + EXPECT_EQ(count.value(), 3U); cleanup_environment(); } -TEST(kvs_flush, flush_success_data) +TEST(kvs_snapshot_count, snapshot_count_non_contiguous) { prepare_environment(); - /* Test flush with valid data */ - system(("rm -rf " + kvs_prefix + ".json").c_str()); - system(("rm -rf " + kvs_prefix + ".hash").c_str()); auto result = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); ASSERT_TRUE(result); - result.value().kvs.clear(); /* Clear KVS to ensure no data is written */ - std::string value = "value1"; - result.value().kvs.insert({"key1", KvsValue(value)}); - auto flush_result = result.value().flush(); - ASSERT_TRUE(flush_result); - - /* Check if files were created correctly */ - EXPECT_TRUE(std::filesystem::exists(kvs_prefix + ".json")); - EXPECT_TRUE(std::filesystem::exists(kvs_prefix + ".hash")); - EXPECT_FALSE(std::filesystem::exists(filename_prefix + "_1.json")); - EXPECT_FALSE(std::filesystem::exists(filename_prefix + "_1.hash")); + std::ofstream(filename_prefix + "_1.json") << "{}"; + std::ofstream(filename_prefix + "_3.json") << "{}"; + auto count = result.value().snapshot_count(); + EXPECT_TRUE(count); + EXPECT_EQ(count.value(), 2U); cleanup_environment(); } -TEST(kvs_flush, flush_success_snapshot_rotate) +TEST(kvs_snapshot_count, snapshot_count_invalid) { prepare_environment(); - /* Test flush with valid data */ - system(("rm -rf " + kvs_prefix + ".json").c_str()); - system(("rm -rf " + kvs_prefix + ".hash").c_str()); - - auto result = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); - ASSERT_TRUE(result); - EXPECT_FALSE(std::filesystem::exists(filename_prefix + "_1.json")); - EXPECT_FALSE(std::filesystem::exists(filename_prefix + "_1.hash")); - - result.value().flush(); /* Initial Flush -> SnapshotID 0 */ + auto kvs = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); + ASSERT_TRUE(kvs); - /* Check if snapshot_rotate was triggered on second flush --> one snapshot should be available - * afterwards */ - auto flush_result = result.value().flush(); - ASSERT_TRUE(flush_result); - EXPECT_TRUE(std::filesystem::exists(filename_prefix + "_1.json")); - EXPECT_TRUE(std::filesystem::exists(filename_prefix + "_1.hash")); + /* Mock Filesystem: Exists fails on first call */ + score::filesystem::Filesystem mock_filesystem = score::filesystem::CreateMockFileSystem(); + auto standard_mock = std::dynamic_pointer_cast(mock_filesystem.standard); + ASSERT_NE(standard_mock, nullptr); + EXPECT_CALL(*standard_mock, Exists(::testing::_)) + .WillOnce(::testing::Return( + score::Result(score::MakeUnexpected(score::filesystem::ErrorCode::kCouldNotRetrieveStatus)))); + kvs.value().filesystem = std::make_unique(std::move(mock_filesystem)); - cleanup_environment(); + auto result = kvs.value().snapshot_count(); + EXPECT_FALSE(result); + EXPECT_EQ(static_cast(*result.error()), ErrorCode::PhysicalStorageFailure); } -TEST(kvs_flush, flush_failure_mutex) + +TEST(kvs_snapshot_create, snapshot_create_success) { prepare_environment(); - auto result = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); + auto kvs = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); + ASSERT_TRUE(kvs); + + /* No snapshot files exist: first free slot is 1. + Source _0.json / _0.hash are created by prepare_environment(). */ + auto result = kvs.value().snapshot_create(); ASSERT_TRUE(result); + EXPECT_EQ(result.value(), 1U); - std::unique_lock lock(result.value().kvs_mutex); - auto flush_result = result.value().flush(); - EXPECT_FALSE(flush_result); - EXPECT_EQ(static_cast(*flush_result.error()), ErrorCode::MutexLockFailed); + /* Verify the snapshot files were physically created */ + EXPECT_TRUE(std::filesystem::exists(filename_prefix + "_1.json")); + EXPECT_TRUE(std::filesystem::exists(filename_prefix + "_1.hash")); cleanup_environment(); } -TEST(kvs_flush, flush_failure_rotate_snapshots) +TEST(kvs_snapshot_create, snapshot_create_failure_snapshot_count) { prepare_environment(); - /* Test Folder for permission handling */ - std::string permissions_dir = data_dir + "permissions/"; - std::filesystem::create_directories(permissions_dir); - auto result = - Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(permissions_dir)); - ASSERT_TRUE(result); - std::filesystem::permissions( - permissions_dir, std::filesystem::perms::owner_read, std::filesystem::perm_options::replace); - auto flush_result = result.value().flush(); + auto kvs = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); + ASSERT_TRUE(kvs); - EXPECT_FALSE(flush_result); - EXPECT_EQ(static_cast(*flush_result.error()), ErrorCode::PhysicalStorageFailure); + /* Mock Filesystem: first Exists() call fails -> snapshot_count() returns error */ + score::filesystem::Filesystem mock_filesystem = score::filesystem::CreateMockFileSystem(); + auto standard_mock = std::dynamic_pointer_cast(mock_filesystem.standard); + ASSERT_NE(standard_mock, nullptr); + EXPECT_CALL(*standard_mock, Exists(::testing::_)) + .WillOnce(::testing::Return( + score::Result(score::MakeUnexpected(score::filesystem::ErrorCode::kCouldNotRetrieveStatus)))); + kvs.value().filesystem = std::make_unique(std::move(mock_filesystem)); + + auto result = kvs.value().snapshot_create(); + EXPECT_FALSE(result); + EXPECT_EQ(static_cast(*result.error()), ErrorCode::PhysicalStorageFailure); cleanup_environment(); } -TEST(kvs_flush, flush_failure_kvsvalue_invalid) +TEST(kvs_snapshot_create, snapshot_create_quota_exceeded) { prepare_environment(); - auto result = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); - ASSERT_TRUE(result); + auto kvs = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); + ASSERT_TRUE(kvs); - BrokenKvsValue invalid; - result.value().kvs.insert({"invalid_key", invalid}); + /* Fill all KVS_MAX_SNAPSHOTS slots */ + for (std::size_t i = 1; i <= KVS_MAX_SNAPSHOTS; i++) + { + std::ofstream(filename_prefix + "_" + std::to_string(i) + ".json") << "{}"; + } - auto flush_result_invalid = result.value().flush(); - EXPECT_FALSE(flush_result_invalid); - EXPECT_EQ(flush_result_invalid.error(), ErrorCode::InvalidValueType); + /* snapshot_count == KVS_MAX_SNAPSHOTS >= snapshot_max_count() */ + auto result = kvs.value().snapshot_create(); + EXPECT_FALSE(result); + EXPECT_EQ(static_cast(*result.error()), ErrorCode::QuotaExceeded); cleanup_environment(); } -TEST(kvs_flush, flush_failure_json_writer) +TEST(kvs_snapshot_create, snapshot_create_failure_first_free_slot) { prepare_environment(); auto kvs = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); ASSERT_TRUE(kvs); - auto mock_writer = std::make_unique(); /* Force error in writer.ToBuffer */ - EXPECT_CALL(*mock_writer, ToBuffer(::testing::A())) - .WillOnce( - ::testing::Return(score::Result(score::MakeUnexpected(score::json::Error::kUnknownError)))); + /* Mock Filesystem: + * - snapshot_count(): first call returns false (gap) -> stops immediately, count = 0 + * - first_free_slot(): first call returns I/O error */ + score::filesystem::Filesystem mock_filesystem = score::filesystem::CreateMockFileSystem(); + auto standard_mock = std::dynamic_pointer_cast(mock_filesystem.standard); + ASSERT_NE(standard_mock, nullptr); + EXPECT_CALL(*standard_mock, Exists(::testing::_)) + .WillOnce(::testing::Return(score::Result(false))) + .WillOnce(::testing::Return( + score::Result(score::MakeUnexpected(score::filesystem::ErrorCode::kCouldNotRetrieveStatus)))); + kvs.value().filesystem = std::make_unique(std::move(mock_filesystem)); - kvs->writer = std::move(mock_writer); - auto result = kvs.value().flush(); + auto result = kvs.value().snapshot_create(); EXPECT_FALSE(result); - EXPECT_EQ(result.error(), ErrorCode::JsonGeneratorError); + EXPECT_EQ(static_cast(*result.error()), ErrorCode::PhysicalStorageFailure); cleanup_environment(); } -TEST(kvs_snapshot_count, snapshot_count_success) +TEST(kvs_snapshot_create, snapshot_create_failure_copy_json) { prepare_environment(); - auto result = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); - ASSERT_TRUE(result); + auto kvs = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); + ASSERT_TRUE(kvs); - /* Create empty Test-Snapshot Files */ - for (size_t i = 0; i < KVS_MAX_SNAPSHOTS; i++) - { - std::ofstream(filename_prefix + "_" + std::to_string(i) + ".json") << "{}"; - auto count = result.value().snapshot_count(); - EXPECT_TRUE(count); - EXPECT_EQ(count.value(), i+1); - } - /* Test maximum capacity */ - std::ofstream(filename_prefix + "_" + std::to_string(KVS_MAX_SNAPSHOTS + 1) + ".json") << "{}"; - auto count = result.value().snapshot_count(); - EXPECT_TRUE(count); - EXPECT_EQ(count.value(), KVS_MAX_SNAPSHOTS); + /* Mock Filesystem: + * - snapshot_count(): all slots are missing -> count = 0 + * - first_free_slot(): first slot is free + * - CopyFile (JSON): fails */ + score::filesystem::Filesystem mock_filesystem = score::filesystem::CreateMockFileSystem(); + auto standard_mock = std::dynamic_pointer_cast(mock_filesystem.standard); + ASSERT_NE(standard_mock, nullptr); + EXPECT_CALL(*standard_mock, Exists(::testing::_)) + .WillRepeatedly(::testing::Return(score::Result(false))); + EXPECT_CALL(*standard_mock, CopyFile(::testing::_, ::testing::_)) + .WillOnce(::testing::Return( + score::ResultBlank(score::MakeUnexpected(score::filesystem::ErrorCode::kCouldNotCreateFile)))); + kvs.value().filesystem = std::make_unique(std::move(mock_filesystem)); + + auto result = kvs.value().snapshot_create(); + EXPECT_FALSE(result); + EXPECT_EQ(static_cast(*result.error()), ErrorCode::PhysicalStorageFailure); cleanup_environment(); } -TEST(kvs_snapshot_count, snapshot_count_invalid) +TEST(kvs_snapshot_create, snapshot_create_failure_copy_hash) { prepare_environment(); auto kvs = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); ASSERT_TRUE(kvs); - /* Mock Filesystem */ + /* Mock Filesystem: + * - snapshot_count(): all slots are missing -> count = 0 + * - first_free_slot(): first slot is free + * - CopyFile (JSON): succeeds + * - CopyFile (hash): fails */ score::filesystem::Filesystem mock_filesystem = score::filesystem::CreateMockFileSystem(); auto standard_mock = std::dynamic_pointer_cast(mock_filesystem.standard); ASSERT_NE(standard_mock, nullptr); EXPECT_CALL(*standard_mock, Exists(::testing::_)) + .WillRepeatedly(::testing::Return(score::Result(false))); + EXPECT_CALL(*standard_mock, CopyFile(::testing::_, ::testing::_)) + .WillOnce(::testing::Return(score::ResultBlank{})) .WillOnce(::testing::Return( - score::Result(score::MakeUnexpected(score::filesystem::ErrorCode::kCouldNotRetrieveStatus)))); + score::ResultBlank(score::MakeUnexpected(score::filesystem::ErrorCode::kCouldNotCreateFile)))); kvs.value().filesystem = std::make_unique(std::move(mock_filesystem)); - auto result = kvs.value().snapshot_count(); + auto result = kvs.value().snapshot_create(); EXPECT_FALSE(result); EXPECT_EQ(static_cast(*result.error()), ErrorCode::PhysicalStorageFailure); + + cleanup_environment(); } TEST(kvs_snapshot_restore, snapshot_restore_success) @@ -1040,7 +1052,7 @@ TEST(kvs_snapshot_restore, snapshot_restore_success) hash_out.write(reinterpret_cast(hash_bytes.data()), hash_bytes.size()); hash_out.close(); - auto restore_result = result.value().snapshot_restore(1); + auto restore_result = result.value().snapshot_restore(0); EXPECT_TRUE(restore_result); EXPECT_TRUE(result.value().kvs.count("kvs_old")); @@ -1054,7 +1066,7 @@ TEST(kvs_snapshot_restore, snapshot_restore_failure_invalid_snapshot_id) auto result = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); ASSERT_TRUE(result); - /* Restore Snapshot ID 0 -> Current KVS*/ + /* Restore Snapshot ID 0 */ auto restore_result = result.value().snapshot_restore(0); ASSERT_FALSE(restore_result); EXPECT_EQ(static_cast(*restore_result.error()), ErrorCode::InvalidSnapshotId); @@ -1078,7 +1090,7 @@ TEST(kvs_snapshot_restore, snapshot_restore_failure_open_json) std::ofstream(filename_prefix + "_1.json") << "{}"; /* Empty JSON */ std::ofstream(filename_prefix + "_1.hash") << "invalid_hash"; /* Invalid Hash -> Trigger open_json error */ - auto restore_result = result.value().snapshot_restore(1); + auto restore_result = result.value().snapshot_restore(0); EXPECT_FALSE(restore_result); EXPECT_EQ(static_cast(*restore_result.error()), ErrorCode::ValidationFailed); /* passed by open_json*/ @@ -1122,6 +1134,240 @@ TEST(kvs_snapshot_restore, snapshot_restore_failure_snapshot_count) cleanup_environment(); } +TEST(kvs_snapshot_delete, snapshot_delete_success) +{ + prepare_environment(); + + auto kvs = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); + ASSERT_TRUE(kvs); + + /* Create snapshot files in slot 1 (id=0 maps to _1.json/_1.hash) */ + std::ofstream(filename_prefix + "_1.json") << "{}"; + std::ofstream(filename_prefix + "_1.hash") << "hash"; + + auto result = kvs.value().snapshot_delete(0); + EXPECT_TRUE(result); + + /* Both snapshot files must be gone */ + EXPECT_FALSE(std::filesystem::exists(filename_prefix + "_1.json")); + EXPECT_FALSE(std::filesystem::exists(filename_prefix + "_1.hash")); + + cleanup_environment(); +} + +TEST(kvs_snapshot_delete, snapshot_delete_failure_mutex) +{ + prepare_environment(); + + auto kvs = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); + ASSERT_TRUE(kvs); + + std::unique_lock lock(kvs.value().kvs_mutex); + auto result = kvs.value().snapshot_delete(0); + EXPECT_FALSE(result); + EXPECT_EQ(static_cast(*result.error()), ErrorCode::MutexLockFailed); + + cleanup_environment(); +} + +TEST(kvs_snapshot_delete, snapshot_delete_failure_invalid_id_no_snapshots) +{ + prepare_environment(); + + auto kvs = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); + ASSERT_TRUE(kvs); + + /* No snapshot files exist -> count == 0 -> any id is invalid + */ + auto result = kvs.value().snapshot_delete(1); + EXPECT_FALSE(result); + EXPECT_EQ(static_cast(*result.error()), ErrorCode::InvalidSnapshotId); + + cleanup_environment(); +} + +TEST(kvs_snapshot_delete, snapshot_delete_failure_invalid_id_too_large) +{ + prepare_environment(); + + auto kvs = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); + ASSERT_TRUE(kvs); + + /* Create a snapshot so count > 0, but pass an id >= snapshot_max_count() */ + std::ofstream(filename_prefix + "_1.json") << "{}"; + + auto result = kvs.value().snapshot_delete(KVS_MAX_SNAPSHOTS); + EXPECT_FALSE(result); + EXPECT_EQ(static_cast(*result.error()), ErrorCode::InvalidSnapshotId); + + cleanup_environment(); +} + +TEST(kvs_snapshot_delete, snapshot_delete_failure_snapshot_count) +{ + prepare_environment(); + + auto kvs = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); + ASSERT_TRUE(kvs); + + /* Mock Filesystem: first Exists() call fails -> snapshot_count() returns error */ + score::filesystem::Filesystem mock_filesystem = score::filesystem::CreateMockFileSystem(); + auto standard_mock = std::dynamic_pointer_cast(mock_filesystem.standard); + ASSERT_NE(standard_mock, nullptr); + EXPECT_CALL(*standard_mock, Exists(::testing::_)) + .WillOnce(::testing::Return( + score::Result(score::MakeUnexpected(score::filesystem::ErrorCode::kCouldNotRetrieveStatus)))); + kvs.value().filesystem = std::make_unique(std::move(mock_filesystem)); + + auto result = kvs.value().snapshot_delete(0); + EXPECT_FALSE(result); + EXPECT_EQ(static_cast(*result.error()), ErrorCode::PhysicalStorageFailure); + + cleanup_environment(); +} + +TEST(kvs_snapshot_delete, snapshot_delete_failure_exists) +{ + prepare_environment(); + + auto kvs = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); + ASSERT_TRUE(kvs); + + /* Mock Filesystem: + * - snapshot_count(): _1.json exists (true) -> count=1; _2.json missing (false) -> stop + * - Exists(json_path for id=0 -> _1.json): I/O error */ + score::filesystem::Filesystem mock_filesystem = score::filesystem::CreateMockFileSystem(); + auto standard_mock = std::dynamic_pointer_cast(mock_filesystem.standard); + ASSERT_NE(standard_mock, nullptr); + EXPECT_CALL(*standard_mock, Exists(::testing::_)) + .WillOnce(::testing::Return(score::Result(true))) + .WillOnce(::testing::Return(score::Result(false))) + .WillOnce(::testing::Return( + score::Result(score::MakeUnexpected(score::filesystem::ErrorCode::kCouldNotRetrieveStatus)))); + kvs.value().filesystem = std::make_unique(std::move(mock_filesystem)); + + auto result = kvs.value().snapshot_delete(0); + EXPECT_FALSE(result); + EXPECT_EQ(static_cast(*result.error()), ErrorCode::PhysicalStorageFailure); + + cleanup_environment(); +} + +TEST(kvs_snapshot_delete, snapshot_delete_failure_not_found) +{ + prepare_environment(); + + auto kvs = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); + ASSERT_TRUE(kvs); + + /* Mock Filesystem: + * - snapshot_count(): exactly one slot exists (first call true, remaining count calls false) + * - Exists(json_path for id=0 -> _1.json): returns false -> file not found */ + score::filesystem::Filesystem mock_filesystem = score::filesystem::CreateMockFileSystem(); + auto standard_mock = std::dynamic_pointer_cast(mock_filesystem.standard); + ASSERT_NE(standard_mock, nullptr); + std::size_t exists_call = 0U; + EXPECT_CALL(*standard_mock, Exists(::testing::_)).WillRepeatedly([&exists_call](const auto&) -> score::Result { + ++exists_call; + if (exists_call == 1U) + { + return score::Result(true); + } + if (exists_call <= KVS_MAX_SNAPSHOTS) + { + return score::Result(false); + } + return score::Result(false); + }); + kvs.value().filesystem = std::make_unique(std::move(mock_filesystem)); + + auto result = kvs.value().snapshot_delete(0); + EXPECT_FALSE(result); + EXPECT_EQ(static_cast(*result.error()), ErrorCode::InvalidSnapshotId); + + cleanup_environment(); +} + +TEST(kvs_snapshot_delete, snapshot_delete_failure_remove_json) +{ + prepare_environment(); + + auto kvs = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); + ASSERT_TRUE(kvs); + + /* Mock Filesystem: + * - snapshot_count(): exactly one slot exists (first call true, remaining count calls false) + * - Exists(json_path): true + * - Remove(json_path): fails */ + score::filesystem::Filesystem mock_filesystem = score::filesystem::CreateMockFileSystem(); + auto standard_mock = std::dynamic_pointer_cast(mock_filesystem.standard); + ASSERT_NE(standard_mock, nullptr); + std::size_t exists_call = 0U; + EXPECT_CALL(*standard_mock, Exists(::testing::_)).WillRepeatedly([&exists_call](const auto&) -> score::Result { + ++exists_call; + if (exists_call == 1U) + { + return score::Result(true); + } + if (exists_call <= KVS_MAX_SNAPSHOTS) + { + return score::Result(false); + } + return score::Result(true); + }); + EXPECT_CALL(*standard_mock, Remove(::testing::_)) + .WillOnce(::testing::Return( + score::ResultBlank(score::MakeUnexpected(score::filesystem::ErrorCode::kCouldNotRemoveFileOrDirectory)))); + kvs.value().filesystem = std::make_unique(std::move(mock_filesystem)); + + auto result = kvs.value().snapshot_delete(0); + EXPECT_FALSE(result); + EXPECT_EQ(static_cast(*result.error()), ErrorCode::PhysicalStorageFailure); + + cleanup_environment(); +} + +TEST(kvs_snapshot_delete, snapshot_delete_failure_remove_hash) +{ + prepare_environment(); + + auto kvs = Kvs::open(instance_id, OpenNeedDefaults::Optional, OpenNeedKvs::Optional, std::string(data_dir)); + ASSERT_TRUE(kvs); + + /* Mock Filesystem: + * - snapshot_count(): exactly one slot exists (first call true, remaining count calls false) + * - Exists(json_path): true + * - Remove(json_path): succeeds + * - Remove(hash_path): fails */ + score::filesystem::Filesystem mock_filesystem = score::filesystem::CreateMockFileSystem(); + auto standard_mock = std::dynamic_pointer_cast(mock_filesystem.standard); + ASSERT_NE(standard_mock, nullptr); + std::size_t exists_call = 0U; + EXPECT_CALL(*standard_mock, Exists(::testing::_)).WillRepeatedly([&exists_call](const auto&) -> score::Result { + ++exists_call; + if (exists_call == 1U) + { + return score::Result(true); + } + if (exists_call <= KVS_MAX_SNAPSHOTS) + { + return score::Result(false); + } + return score::Result(true); + }); + EXPECT_CALL(*standard_mock, Remove(::testing::_)) + .WillOnce(::testing::Return(score::ResultBlank{})) + .WillOnce(::testing::Return( + score::ResultBlank(score::MakeUnexpected(score::filesystem::ErrorCode::kCouldNotRemoveFileOrDirectory)))); + kvs.value().filesystem = std::make_unique(std::move(mock_filesystem)); + + auto result = kvs.value().snapshot_delete(0); + EXPECT_FALSE(result); + EXPECT_EQ(static_cast(*result.error()), ErrorCode::PhysicalStorageFailure); + + cleanup_environment(); +} + TEST(kvs_snapshot_max_count, snapshot_max_count) { prepare_environment(); diff --git a/tests/test_cases/tests/test_cit_snapshots.py b/tests/test_cases/tests/test_cit_snapshots.py index 3173431e..8bec4ac2 100644 --- a/tests/test_cases/tests/test_cit_snapshots.py +++ b/tests/test_cases/tests/test_cit_snapshots.py @@ -40,7 +40,10 @@ def temp_dir( @add_test_properties( - partially_verifies=["comp_req__kvs__snapshot_creation"], + partially_verifies=[ + "comp_req__kvs__snapshot_creation", + "comp_req__kvs__snapshot_explicit_creation", + ], test_type="requirements-based", derivation_technique="requirements-analysis", ) @@ -71,9 +74,9 @@ def test_ok( snapshot_max_count: int, version: str, ): - if version == "cpp" and snapshot_max_count in [0, 1, 3, 10]: + if version == "cpp" and snapshot_max_count == 0: pytest.xfail( - reason="https://github.com/eclipse-score/persistency/issues/108", + reason="C++ snapshot_max_count is hardcoded to 3 and cannot be configured to 0", ) assert results.return_code == ResultCode.SUCCESS @@ -88,7 +91,10 @@ def test_ok( @add_test_properties( - partially_verifies=["comp_req__kvs__snapshot_creation"], + partially_verifies=[ + "comp_req__kvs__snapshot_creation", + "comp_req__kvs__snapshot_explicit_creation", + ], test_type="requirements-based", derivation_technique="requirements-analysis", ) @@ -106,6 +112,20 @@ def test_config(self, temp_dir: Path, snapshot_max_count: int) -> dict[str, Any] "count": snapshot_max_count + 1, } + def test_ok( + self, + test_config: dict[str, Any], + results: ScenarioResult, + logs_info_level: LogContainer, + snapshot_max_count: int, + version: str, + ): + if version == "cpp" and snapshot_max_count != 3: + pytest.xfail( + reason="C++ snapshot_max_count is hardcoded to 3 and cannot be configured otherwise", + ) + super().test_ok(test_config, results, logs_info_level, snapshot_max_count, version) + @add_test_properties( partially_verifies=["comp_req__kvs__snapshot_max_num"], @@ -137,9 +157,9 @@ def test_ok( snapshot_max_count: int, version: str, ): - if version == "cpp": + if version == "cpp" and snapshot_max_count != 3: pytest.xfail( - reason="https://github.com/eclipse-score/persistency/issues/108", + reason="C++ snapshot_max_count is hardcoded to 3 and cannot be configured otherwise", ) assert results.return_code == ResultCode.SUCCESS assert logs_info_level.find_log("max_count", value=snapshot_max_count) is not None @@ -150,6 +170,7 @@ def test_ok( partially_verifies=[ "comp_req__kvs__snapshot_creation", "comp_req__kvs__snapshot_rotate", + "comp_req__kvs__snapshot_explicit_creation", ], test_type="control-flow-analysis", derivation_technique="requirements-analysis", @@ -178,7 +199,13 @@ def test_ok( self, results: ScenarioResult, logs_info_level: LogContainer, + snapshot_max_count: int, + version: str, ): + if version == "cpp" and snapshot_max_count != 3: + pytest.xfail( + reason="C++ snapshot_max_count is hardcoded to 3 and cannot be configured otherwise", + ) assert results.return_code == ResultCode.SUCCESS result_log = logs_info_level.find_log("result") @@ -218,6 +245,11 @@ def test_error( ): assert results.return_code == ResultCode.SUCCESS + if version == "cpp": + pytest.xfail( + reason="In C++ new API snapshot_id=0 refers to the oldest snapshot (valid), not the current state", + ) + if version == "rust": assert "Restoring current KVS snapshot is not allowed" in results.stdout @@ -229,6 +261,7 @@ def test_error( @add_test_properties( partially_verifies=[ "comp_req__kvs__snapshot_creation", + "comp_req__kvs__snapshot_explicit_creation", "comp_req__kvs__snapshot_restore", ], test_type="fault-injection", @@ -261,7 +294,10 @@ def test_error(self, results: ScenarioResult, logs_info_level: LogContainer, ver @add_test_properties( - partially_verifies=["comp_req__kvs__snapshot_creation"], + partially_verifies=[ + "comp_req__kvs__snapshot_creation", + "comp_req__kvs__snapshot_explicit_creation", + ], test_type="interface-test", derivation_technique="requirements-analysis", ) @@ -297,7 +333,10 @@ def test_ok( @add_test_properties( - partially_verifies=["comp_req__kvs__snapshot_creation"], + partially_verifies=[ + "comp_req__kvs__snapshot_creation", + "comp_req__kvs__snapshot_explicit_creation", + ], test_type="fault-injection", derivation_technique="requirements-analysis", ) @@ -330,3 +369,178 @@ def test_error( assert not Path(paths_log.kvs_path).exists() assert paths_log.hash_path == f"{temp_dir}/kvs_1_2.hash" assert not Path(paths_log.hash_path).exists() + + +@add_test_properties( + fully_verifies=["comp_req__kvs__snapshot_explicit_creation"], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +class TestSnapshotCreateExplicit(CommonScenario): + """Verifies that in C++ flush alone does not create a snapshot; only snapshot_create does.""" + + @pytest.fixture(scope="class") + def scenario_name(self) -> str: + return "cit.snapshots.create" + + @pytest.fixture(scope="class") + def test_config(self, temp_dir: Path) -> dict[str, Any]: + return { + "kvs_parameters": {"instance_id": 1, "dir": str(temp_dir)}, + } + + def test_ok( + self, + results: ScenarioResult, + logs_info_level: LogContainer, + version: str, + ): + if version != "cpp": + pytest.xfail( + reason="snapshot_create is a C++ explicit API; Rust creates snapshots automatically on flush", + ) + assert results.return_code == ResultCode.SUCCESS + + # After flush only: no snapshot must exist yet. + flush_log = logs_info_level.find_log("snapshot_count_after_flush") + assert flush_log is not None + assert flush_log.snapshot_count_after_flush == 0 + + # snapshot_create must succeed. + result_log = logs_info_level.find_log("result") + assert result_log is not None + assert result_log.result == "Ok(())" + + # After snapshot_create: exactly one snapshot must exist. + create_log = logs_info_level.find_log("snapshot_count_after_create") + assert create_log is not None + assert create_log.snapshot_count_after_create == 1 + + +@add_test_properties( + fully_verifies=["comp_req__kvs__snapshot_delete"], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +class TestSnapshotDeleteExisting(CommonScenario): + """Verifies that an existing snapshot can be deleted and the snapshot count decreases.""" + + @pytest.fixture(scope="class") + def scenario_name(self) -> str: + return "cit.snapshots.delete" + + @pytest.fixture(scope="class") + def test_config(self, temp_dir: Path) -> dict[str, Any]: + return { + "kvs_parameters": {"instance_id": 1, "dir": str(temp_dir)}, + "snapshot_id": 1, + "count": 2, + } + + def test_ok( + self, + results: ScenarioResult, + logs_info_level: LogContainer, + version: str, + ): + if version != "cpp": + pytest.xfail( + reason="snapshot_delete is a C++ explicit API; Rust does not support snapshot deletion", + ) + assert results.return_code == ResultCode.SUCCESS + + result_log = logs_info_level.find_log("result") + assert result_log is not None + assert result_log.result == "Ok(())" + + count_log = logs_info_level.find_log("snapshot_count") + assert count_log is not None + assert count_log.snapshot_count == 1 + + +@add_test_properties( + partially_verifies=["comp_req__kvs__snapshot_delete"], + test_type="fault-injection", + derivation_technique="requirements-analysis", +) +class TestSnapshotDeleteNonexistent(CommonScenario): + """Checks that deleting a non-existing snapshot fails with InvalidSnapshotId error.""" + + @pytest.fixture(scope="class") + def scenario_name(self) -> str: + return "cit.snapshots.delete" + + @pytest.fixture(scope="class") + def test_config(self, temp_dir: Path) -> dict[str, Any]: + return { + "kvs_parameters": {"instance_id": 1, "dir": str(temp_dir)}, + "snapshot_id": 2, + "count": 1, + } + + def test_error( + self, + results: ScenarioResult, + logs_info_level: LogContainer, + version: str, + ): + if version != "cpp": + pytest.xfail( + reason="snapshot_delete is a C++ explicit API; Rust does not support snapshot deletion", + ) + assert results.return_code == ResultCode.SUCCESS + + result_log = logs_info_level.find_log("result") + assert result_log is not None + assert result_log.result == "Err(InvalidSnapshotId)" + + +@add_test_properties( + fully_verifies=["comp_req__kvs__snapshot_rotate"], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +@pytest.mark.parametrize("snapshot_max_count", [2, 3], scope="class") +class TestSnapshotRotate(MaxSnapshotsScenario): + """Verifies that the oldest snapshot is rotated out when max count is reached on flush.""" + + @pytest.fixture(scope="class") + def scenario_name(self) -> str: + return "cit.snapshots.rotate" + + @pytest.fixture(scope="class") + def test_config(self, temp_dir: Path, snapshot_max_count: int) -> dict[str, Any]: + return { + "kvs_parameters": { + "instance_id": 1, + "dir": str(temp_dir), + "snapshot_max_count": snapshot_max_count, + }, + "count": snapshot_max_count + 1, + } + + def test_ok( + self, + results: ScenarioResult, + logs_info_level: LogContainer, + snapshot_max_count: int, + version: str, + ): + pytest.xfail( + reason="snapshot rotation is no longer valid in C++ and not yet implemented in Rust", + ) + assert results.return_code == ResultCode.SUCCESS + + # After max_count+1 flushes, snapshot_count must equal max_count (oldest rotated out). + count_log = logs_info_level.find_log("snapshot_count") + assert count_log is not None + assert count_log.snapshot_count == snapshot_max_count + + # Oldest surviving snapshot must be counter=1 (counter=0 was rotated out). + result_log = logs_info_level.find_log("result") + assert result_log is not None + assert result_log.result == "Ok(())" + + value_log = logs_info_level.find_log("value") + assert value_log is not None + assert value_log.value == 1 diff --git a/tests/test_scenarios/cpp/src/cit/snapshots.cpp b/tests/test_scenarios/cpp/src/cit/snapshots.cpp index c941602b..24e87a27 100644 --- a/tests/test_scenarios/cpp/src/cit/snapshots.cpp +++ b/tests/test_scenarios/cpp/src/cit/snapshots.cpp @@ -64,28 +64,6 @@ class SnapshotCount : public Scenario return "count"; } - /** - * Requirement not being met: - * - The snapshot is created for each data stored. - * - Max count should be configurable. - * - * TestSnapshotCountFirstFlush - * Issue: The test expects the final snapshot_count to be min(count, - * snapshot_max_count) (e.g., 1 for count=1, snapshot_max_count=1/3/10). - * Observed: C++ emits snapshot_count: 0 after the first flush. - * Possible Root Cause: In C++, the snapshot count is not incremented after - * the first flush because the snapshot rotation logic and counting are tied to - * the hardcoded max (not the parameter). - * - * TestSnapshotCountFull - * Issue: The test expects a sequence of snapshot_count values: [0, 1] for count=2, [0, 1, - * 2, 3] for count=4, etc. Observed: C++ emits [0, 0, 1] or [0, 0, 1, 2, 3], but the first value - * is always 0, and the final value is not as expected. Possible Root Cause: The C++ - * implementation may not be accumulating the count correctly, it stores or updates the count - * only after flush when MAX<3. - * - * Raised bugs: https://github.com/eclipse-score/persistency/issues/108 - */ void run(const std::string& input) const final { auto obj{get_object(input)}; @@ -116,6 +94,9 @@ class SnapshotCount : public Scenario { throw std::runtime_error{"Failed to flush"}; } + + // Create snapshot in the first available slot. Ignore failure if max count is reached. + kvs.snapshot_create(); } { @@ -142,8 +123,6 @@ class SnapshotMaxCount : public Scenario void run(const std::string& input) const final { - auto obj{get_object(input)}; - auto count{get_field(obj, "count")}; auto params{KvsParameters::from_json(input)}; auto kvs{kvs_instance(params)}; @@ -184,6 +163,13 @@ class SnapshotRestore : public Scenario { throw std::runtime_error{"Failed to flush"}; } + + // Create snapshot in the first available slot. + auto create_result{kvs.snapshot_create()}; + if (!create_result) + { + throw std::runtime_error{"Failed to create snapshot"}; + } } { @@ -243,6 +229,13 @@ class SnapshotPaths : public Scenario { throw std::runtime_error{"Failed to flush"}; } + + // Create snapshot in the first available slot. + auto create_result{kvs.snapshot_create()}; + if (!create_result) + { + throw std::runtime_error{"Failed to create snapshot"}; + } } { @@ -254,12 +247,127 @@ class SnapshotPaths : public Scenario } }; +class SnapshotCreate : public Scenario +{ + public: + ~SnapshotCreate() final = default; + + std::string name() const final + { + return "create"; + } + + void run(const std::string& input) const final + { + auto params{KvsParameters::from_json(input)}; + + auto kvs{kvs_instance(params)}; + auto set_result{kvs.set_value("counter", KvsValue{42})}; + if (!set_result) + { + throw std::runtime_error{"Failed to set value"}; + } + + // Flush only: in C++ this does NOT create a snapshot. + auto flush_result{kvs.flush()}; + if (!flush_result) + { + throw std::runtime_error{"Failed to flush"}; + } + + auto count_after_flush{kvs.snapshot_count()}; + if (!count_after_flush) + { + throw std::runtime_error{"Unable to get snapshot count"}; + } + TRACING_INFO(kTargetName, + std::pair{std::string{"snapshot_count_after_flush"}, count_after_flush.value()}); + + // Explicitly create snapshot. + auto create_result{kvs.snapshot_create()}; + TRACING_INFO(kTargetName, + std::pair{std::string{"result"}, create_result ? "Ok(())" : "Err(OutOfStorageSpace)"}); + + auto count_after_create{kvs.snapshot_count()}; + if (!count_after_create) + { + throw std::runtime_error{"Unable to get snapshot count"}; + } + TRACING_INFO(kTargetName, + std::pair{std::string{"snapshot_count_after_create"}, count_after_create.value()}); + } +}; + +class SnapshotDelete : public Scenario +{ + public: + ~SnapshotDelete() final = default; + + std::string name() const final + { + return "delete"; + } + + void run(const std::string& input) const final + { + auto obj{get_object(input)}; + auto count{get_field(obj, "count")}; + auto snapshot_id{get_field(obj, "snapshot_id")}; + auto params{KvsParameters::from_json(input)}; + + // Create snapshots. + for (int32_t i{0}; i < count; ++i) + { + auto kvs{kvs_instance(params)}; + auto set_result{kvs.set_value("counter", KvsValue{i})}; + if (!set_result) + { + throw std::runtime_error{"Failed to set value"}; + } + + // Flush KVS. + auto flush_result{kvs.flush()}; + if (!flush_result) + { + throw std::runtime_error{"Failed to flush"}; + } + + // Create snapshot in the first available slot. + auto create_result{kvs.snapshot_create()}; + if (!create_result) + { + throw std::runtime_error{"Failed to create snapshot"}; + } + } + + { + auto kvs{kvs_instance(params)}; + + auto delete_result{kvs.snapshot_delete(snapshot_id)}; + TRACING_INFO(kTargetName, + std::pair{std::string{"result"}, delete_result ? "Ok(())" : "Err(InvalidSnapshotId)"}); + + if (delete_result) + { + auto count_result{kvs.snapshot_count()}; + if (!count_result) + { + throw std::runtime_error{"Unable to get snapshot count after delete"}; + } + TRACING_INFO(kTargetName, std::pair{std::string{"snapshot_count"}, count_result.value()}); + } + } + } +}; + ScenarioGroup::Ptr snapshots_group() { return ScenarioGroup::Ptr{new ScenarioGroupImpl{"snapshots", {std::make_shared(), std::make_shared(), std::make_shared(), - std::make_shared()}, + std::make_shared(), + std::make_shared(), + std::make_shared()}, {}}}; }