From 5be3a0ddcc12302b239b970ac82da25cfb61c9eb Mon Sep 17 00:00:00 2001 From: Alan King Date: Tue, 23 Jul 2024 17:07:07 -0400 Subject: [PATCH] [#7918] replica_truncate: Add unit test output string checking This change adds assertions about the contents of the output string that is returned from the replica_truncate API. Depending on the error, the message and other contents of the JSON structure could be in a variety of states, so we want to make sure that these hold the expected values in all cases. In the cases where errors do not occur, we want to ensure that the correct replica number and resource hierarchy are returned so that the caller knows which replica was truncated. This change also adds a new test for when a logical path including a zone name which does not exist is targeted for truncate. --- unit_tests/src/test_replica_truncate.cpp | 507 +++++++++++++++++++++-- 1 file changed, 481 insertions(+), 26 deletions(-) diff --git a/unit_tests/src/test_replica_truncate.cpp b/unit_tests/src/test_replica_truncate.cpp index 87bfe574ec..b5d8feec5b 100644 --- a/unit_tests/src/test_replica_truncate.cpp +++ b/unit_tests/src/test_replica_truncate.cpp @@ -18,6 +18,7 @@ #include "irods/rodsErrorTable.h" #include "irods/system_error.hpp" #include "irods/transport/default_transport.hpp" +#include "irods/zone_administration.hpp" #include "unit_test_utils.hpp" #include @@ -40,6 +41,8 @@ namespace replica = irods::experimental::replica; namespace { + constexpr const char* same_size_message = "rs_replica_truncate: Replica of [{}] on [{}] already has size [{}]."; + auto create_replication_resource(RcComm& _comm, const std::string_view _resc_name) -> void { adm::resource_registration_info resc_info; @@ -54,6 +57,7 @@ TEST_CASE("basic_two_standalone_resources") { load_client_api_plugins(); + const std::string default_resc = "demoResc"; const std::string test_resc = "test_resc"; const std::string vault_name = "test_resc_vault"; @@ -122,6 +126,18 @@ TEST_CASE("basic_two_standalone_resources") // Attempt to truncate the object. REQUIRE(0 == rc_replica_truncate(&comm, &truncate_doi, &output_str)); + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + const auto expected_json_output = nlohmann::json{ + {"message", fmt::format(same_size_message, target_object.c_str(), default_resc, contents.size())}, + {"replica_number", 0}, + {"resource_hierarchy", default_resc.c_str()}}; + + CHECK(expected_json_output == json_out); + // Ensure that none of the replicas were updated. CHECK(GOOD_REPLICA == replica::replica_status(comm, target_object, 0)); CHECK(contents.size() == replica::replica_size(comm, target_object, 0)); @@ -139,6 +155,16 @@ TEST_CASE("basic_two_standalone_resources") // Attempt to truncate the object. REQUIRE(0 == rc_replica_truncate(&comm, &truncate_doi, &output_str)); + // Ensure that the returned output string has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + const auto expected_json_output = + nlohmann::json{{"message", ""}, {"replica_number", 0}, {"resource_hierarchy", default_resc.c_str()}}; + + CHECK(expected_json_output == json_out); + // Ensure that the replica on the target resource was updated and the other replica was not updated, // but marked stale. CHECK(GOOD_REPLICA == replica::replica_status(comm, target_object, 0)); @@ -157,6 +183,16 @@ TEST_CASE("basic_two_standalone_resources") // Attempt to truncate the object. REQUIRE(0 == rc_replica_truncate(&comm, &truncate_doi, &output_str)); + // Ensure that the returned output string has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + const auto expected_json_output = + nlohmann::json{{"message", ""}, {"replica_number", 0}, {"resource_hierarchy", default_resc.c_str()}}; + + CHECK(expected_json_output == json_out); + // Ensure that the replica on the target resource was updated and the other replica was not updated, // but marked stale. CHECK(GOOD_REPLICA == replica::replica_status(comm, target_object, 0)); @@ -239,6 +275,12 @@ TEST_CASE("two_replicas_in_replication_resource") REQUIRE(GOOD_REPLICA == replica::replica_status(comm, target_object, 2)); REQUIRE(contents.size() == replica::replica_size(comm, target_object, 2)); + // For this test, we cannot be 100% sure which replica was selected due to database result ordering. So we need to + // query the resource names of the replicas. + const auto replica_0_resource = *replica::to_leaf_resource(comm, target_object, 0); + const auto replica_1_resource = *replica::to_leaf_resource(comm, target_object, 1); + const auto replica_2_resource = *replica::to_leaf_resource(comm, target_object, 2); + // Sleep here so that the mtime on the replica we truncate can be different. const auto original_mtime_replica_0 = replica::last_write_time(comm, target_object, 0); const auto original_mtime_replica_1 = replica::last_write_time(comm, target_object, 1); @@ -263,6 +305,18 @@ TEST_CASE("two_replicas_in_replication_resource") // Attempt to truncate the object. REQUIRE(0 == rc_replica_truncate(&comm, &truncate_doi, &output_str)); + { + // Ensure that the returned output string has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + const auto expected_json_output = + nlohmann::json{{"message", ""}, {"replica_number", 0}, {"resource_hierarchy", replica_0_resource}}; + + CHECK(expected_json_output == json_out); + } + // Ensure that the replica outside of the replication hierarchy is updated. CHECK(GOOD_REPLICA == replica::replica_status(comm, target_object, 0)); CHECK(different_size == replica::replica_size(comm, target_object, 0)); @@ -295,6 +349,20 @@ TEST_CASE("two_replicas_in_replication_resource") // the truncate should not occur and it should not propagate to sibling replicas. REQUIRE(0 == rc_replica_truncate(&comm, &truncate_doi, &output_str)); + { + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + const auto expected_json_output = nlohmann::json{ + {"message", fmt::format(same_size_message, target_object.c_str(), replica_0_resource, different_size)}, + {"replica_number", 0}, + {"resource_hierarchy", replica_0_resource}}; + + CHECK(expected_json_output == json_out); + } + // Ensure that none of the replicas were updated. CHECK(GOOD_REPLICA == replica::replica_status(comm, target_object, 0)); CHECK(different_size == replica::replica_size(comm, target_object, 0)); @@ -319,6 +387,39 @@ TEST_CASE("two_replicas_in_replication_resource") // Attempt to truncate the object. REQUIRE(0 == rc_replica_truncate(&comm, &truncate_doi, &output_str)); + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + // Even though none of the replicas were updated, a replica was selected for truncate, which is how the system + // determined that it was the same size as the requested truncate size. Check to make sure the replica info + // matches what we expect. + + // We cannot expect one replica to have been truncated any more than the other. We must figure out which replica + // was truncated here, by number, and then we can assert which resource hierarchy we expect to see. + const auto replica_number_itr = json_out.find("replica_number"); + REQUIRE(json_out.end() != replica_number_itr); + const auto replica_number = replica_number_itr->get(); + // This is a separate variable because "chained comparisons are not supported inside assertions" in Catch2. + const auto replica_number_is_1_or_2 = (1 == replica_number || 2 == replica_number); + REQUIRE(replica_number_is_1_or_2); + + // Ensure that the resource hierarchy returned matches that of the replica which was truncated. + const auto& expected_leaf_resource = (1 == replica_number) ? replica_1_resource : replica_2_resource; + const auto expected_resource_hierarchy = fmt::format("{};{}", replication_resc.c_str(), expected_leaf_resource); + const auto resource_hierarchy_itr = json_out.find("resource_hierarchy"); + REQUIRE(json_out.end() != resource_hierarchy_itr); + CHECK(expected_resource_hierarchy == resource_hierarchy_itr->get_ref()); + + // Ensure that the message holds the "same size" message. This comes at the end because we need to adjust the + // assertion based on which replica was truncated (which we cannot know ahead of time, as explained above). + const auto message = json_out.find("message"); + CHECK(json_out.end() != message); + const auto expected_message = + fmt::format(same_size_message, target_object.c_str(), expected_resource_hierarchy, contents.size()); + CHECK(expected_message == message->get_ref()); + // Ensure that none of the replicas were updated. CHECK(GOOD_REPLICA == replica::replica_status(comm, target_object, 1)); CHECK(contents.size() == replica::replica_size(comm, target_object, 1)); @@ -345,6 +446,32 @@ TEST_CASE("two_replicas_in_replication_resource") // Attempt to truncate the object. REQUIRE(0 == rc_replica_truncate(&comm, &truncate_doi, &output_str)); + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + // Ensure that the message field is empty. + const auto message = json_out.find("message"); + CHECK(json_out.end() != message); + CHECK(message->get_ref().empty()); + + // We cannot expect one replica to have been truncated any more than the other. We must figure out which replica + // was truncated here, by number, and then we can assert which resource hierarchy we expect to see. + const auto replica_number_itr = json_out.find("replica_number"); + REQUIRE(json_out.end() != replica_number_itr); + const auto replica_number = replica_number_itr->get(); + // This is a separate variable because "chained comparisons are not supported inside assertions" in Catch2. + const auto replica_number_is_1_or_2 = (1 == replica_number || 2 == replica_number); + REQUIRE(replica_number_is_1_or_2); + + // Ensure that the resource hierarchy returned matches that of the replica which was truncated. + const auto& expected_leaf_resource = (1 == replica_number) ? replica_1_resource : replica_2_resource; + const auto expected_resource_hierarchy = fmt::format("{};{}", replication_resc.c_str(), expected_leaf_resource); + const auto resource_hierarchy_itr = json_out.find("resource_hierarchy"); + REQUIRE(json_out.end() != resource_hierarchy_itr); + CHECK(expected_resource_hierarchy == resource_hierarchy_itr->get_ref()); + // Ensure that the replicas in the replication resource hierarchy were updated. CHECK(GOOD_REPLICA == replica::replica_status(comm, target_object, 1)); CHECK(new_size == replica::replica_size(comm, target_object, 1)); @@ -371,6 +498,32 @@ TEST_CASE("two_replicas_in_replication_resource") // Attempt to truncate the object. REQUIRE(0 == rc_replica_truncate(&comm, &truncate_doi, &output_str)); + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + // Ensure that the message field is empty. + const auto message = json_out.find("message"); + CHECK(json_out.end() != message); + CHECK(message->get_ref().empty()); + + // We cannot expect one replica to have been truncated any more than the other. We must figure out which replica + // was truncated here, by number, and then we can assert which resource hierarchy we expect to see. + const auto replica_number_itr = json_out.find("replica_number"); + REQUIRE(json_out.end() != replica_number_itr); + const auto replica_number = replica_number_itr->get(); + // This is a separate variable because "chained comparisons are not supported inside assertions" in Catch2. + const auto replica_number_is_1_or_2 = (1 == replica_number || 2 == replica_number); + REQUIRE(replica_number_is_1_or_2); + + // Ensure that the resource hierarchy returned matches that of the replica which was truncated. + const auto& expected_leaf_resource = (1 == replica_number) ? replica_1_resource : replica_2_resource; + const auto expected_resource_hierarchy = fmt::format("{};{}", replication_resc.c_str(), expected_leaf_resource); + const auto resource_hierarchy_itr = json_out.find("resource_hierarchy"); + REQUIRE(json_out.end() != resource_hierarchy_itr); + CHECK(expected_resource_hierarchy == resource_hierarchy_itr->get_ref()); + // Ensure that the replicas in the replication resource hierarchy were updated. CHECK(GOOD_REPLICA == replica::replica_status(comm, target_object, 1)); CHECK(new_size == replica::replica_size(comm, target_object, 1)); @@ -470,6 +623,9 @@ TEST_CASE("truncate_locked_data_object__issue_7104") char* output_str{}; const auto free_output_str = irods::at_scope_exit{[&output_str] { std::free(output_str); }}; + constexpr const char* error_message_template = + "rs_replica_truncate: Error occurred resolving hierarchy or getting information for [{}]: {}"; + SECTION("target_intermediate_replica") { irods::experimental::client_connection conn2; @@ -479,50 +635,80 @@ TEST_CASE("truncate_locked_data_object__issue_7104") SECTION("by_replica_number") { + constexpr auto expected_error_code = HIERARCHY_ERROR; + addKeyVal(&truncate_doi.condInput, REPL_NUM_KW, "0"); // Attempt to truncate the object using the size specified for each section, and fail. // The assertion occurs inside the sections despite being identical for easier identification. SECTION("same_size") { truncate_doi.dataSize = contents.size(); - CHECK(HIERARCHY_ERROR == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); + CHECK(expected_error_code == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); } SECTION("larger_size") { truncate_doi.dataSize = contents.size() + 1; - CHECK(HIERARCHY_ERROR == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); + CHECK(expected_error_code == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); } SECTION("smaller_size") { truncate_doi.dataSize = contents.size() - 1; - CHECK(HIERARCHY_ERROR == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); + CHECK(expected_error_code == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); } + + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + const auto expected_json_output = nlohmann::json{ + {"message", fmt::format(error_message_template, target_object.c_str(), expected_error_code)}, + {"replica_number", nlohmann::json::value_t::null}, + {"resource_hierarchy", nlohmann::json::value_t::null}}; + + CHECK(expected_json_output == json_out); } SECTION("by_resource_name") { + constexpr auto expected_error_code = HIERARCHY_ERROR; + addKeyVal(&truncate_doi.condInput, RESC_NAME_KW, default_resc.c_str()); // Attempt to truncate the object using the size specified for each section, and fail. // The assertion occurs inside the sections despite being identical for easier identification. SECTION("same_size") { truncate_doi.dataSize = contents.size(); - CHECK(HIERARCHY_ERROR == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); + CHECK(expected_error_code == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); } SECTION("larger_size") { truncate_doi.dataSize = contents.size() + 1; - CHECK(HIERARCHY_ERROR == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); + CHECK(expected_error_code == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); } SECTION("smaller_size") { truncate_doi.dataSize = contents.size() - 1; - CHECK(HIERARCHY_ERROR == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); + CHECK(expected_error_code == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); } + + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + const auto expected_json_output = nlohmann::json{ + {"message", fmt::format(error_message_template, target_object.c_str(), expected_error_code)}, + {"replica_number", nlohmann::json::value_t::null}, + {"resource_hierarchy", nlohmann::json::value_t::null}}; + + CHECK(expected_json_output == json_out); } SECTION("by_resource_hierarchy") { + constexpr auto expected_error_code = INTERMEDIATE_REPLICA_ACCESS; + // This will skip voting. addKeyVal(&truncate_doi.condInput, RESC_HIER_STR_KW, default_resc.c_str()); // Attempt to truncate the object using the size specified for each section, and fail. @@ -530,18 +716,30 @@ TEST_CASE("truncate_locked_data_object__issue_7104") SECTION("same_size") { truncate_doi.dataSize = contents.size(); - CHECK(INTERMEDIATE_REPLICA_ACCESS == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); + CHECK(expected_error_code == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); } SECTION("larger_size") { truncate_doi.dataSize = contents.size() + 1; - CHECK(INTERMEDIATE_REPLICA_ACCESS == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); + CHECK(expected_error_code == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); } SECTION("smaller_size") { truncate_doi.dataSize = contents.size() - 1; - CHECK(INTERMEDIATE_REPLICA_ACCESS == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); + CHECK(expected_error_code == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); } + + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + const auto expected_json_output = nlohmann::json{ + {"message", fmt::format(error_message_template, target_object.c_str(), expected_error_code)}, + {"replica_number", nlohmann::json::value_t::null}, + {"resource_hierarchy", nlohmann::json::value_t::null}}; + + CHECK(expected_json_output == json_out); } } @@ -552,50 +750,80 @@ TEST_CASE("truncate_locked_data_object__issue_7104") SECTION("by_replica_number") { + constexpr auto expected_error_code = HIERARCHY_ERROR; + addKeyVal(&truncate_doi.condInput, REPL_NUM_KW, "1"); // Attempt to truncate the object using the size specified for each section, and fail. // The assertion occurs inside the sections despite being identical for easier identification. SECTION("same_size") { truncate_doi.dataSize = contents.size(); - CHECK(HIERARCHY_ERROR == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); + CHECK(expected_error_code == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); } SECTION("larger_size") { truncate_doi.dataSize = contents.size() + 1; - CHECK(HIERARCHY_ERROR == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); + CHECK(expected_error_code == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); } SECTION("smaller_size") { truncate_doi.dataSize = contents.size() - 1; - CHECK(HIERARCHY_ERROR == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); + CHECK(expected_error_code == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); } + + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + const auto expected_json_output = nlohmann::json{ + {"message", fmt::format(error_message_template, target_object.c_str(), expected_error_code)}, + {"replica_number", nlohmann::json::value_t::null}, + {"resource_hierarchy", nlohmann::json::value_t::null}}; + + CHECK(expected_json_output == json_out); } SECTION("by_resource_name") { + constexpr auto expected_error_code = HIERARCHY_ERROR; + addKeyVal(&truncate_doi.condInput, RESC_NAME_KW, test_resc.c_str()); // Attempt to truncate the object using the size specified for each section, and fail. // The assertion occurs inside the sections despite being identical for easier identification. SECTION("same_size") { truncate_doi.dataSize = contents.size(); - CHECK(HIERARCHY_ERROR == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); + CHECK(expected_error_code == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); } SECTION("larger_size") { truncate_doi.dataSize = contents.size() + 1; - CHECK(HIERARCHY_ERROR == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); + CHECK(expected_error_code == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); } SECTION("smaller_size") { truncate_doi.dataSize = contents.size() - 1; - CHECK(HIERARCHY_ERROR == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); + CHECK(expected_error_code == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); } + + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + const auto expected_json_output = nlohmann::json{ + {"message", fmt::format(error_message_template, target_object.c_str(), expected_error_code)}, + {"replica_number", nlohmann::json::value_t::null}, + {"resource_hierarchy", nlohmann::json::value_t::null}}; + + CHECK(expected_json_output == json_out); } SECTION("by_resource_hierarchy") { + constexpr auto expected_error_code = LOCKED_DATA_OBJECT_ACCESS; + // This will skip voting. addKeyVal(&truncate_doi.condInput, RESC_HIER_STR_KW, test_resc.c_str()); // Attempt to truncate the object using the size specified for each section, and fail. @@ -603,23 +831,37 @@ TEST_CASE("truncate_locked_data_object__issue_7104") SECTION("same_size") { truncate_doi.dataSize = contents.size(); - CHECK(LOCKED_DATA_OBJECT_ACCESS == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); + CHECK(expected_error_code == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); } SECTION("larger_size") { truncate_doi.dataSize = contents.size() + 1; - CHECK(LOCKED_DATA_OBJECT_ACCESS == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); + CHECK(expected_error_code == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); } SECTION("smaller_size") { truncate_doi.dataSize = contents.size() - 1; - CHECK(LOCKED_DATA_OBJECT_ACCESS == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); + CHECK(expected_error_code == rc_replica_truncate(&comm2, &truncate_doi, &output_str)); } + + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + const auto expected_json_output = nlohmann::json{ + {"message", fmt::format(error_message_template, target_object.c_str(), expected_error_code)}, + {"replica_number", nlohmann::json::value_t::null}, + {"resource_hierarchy", nlohmann::json::value_t::null}}; + + CHECK(expected_json_output == json_out); } } SECTION("no_specific_target") { + constexpr auto expected_error_code = HIERARCHY_ERROR; + // Attempt to truncate the object using the size specified for each section, and fail. // The assertion occurs inside the sections despite being identical for easier identification. SECTION("same_size") @@ -637,6 +879,18 @@ TEST_CASE("truncate_locked_data_object__issue_7104") truncate_doi.dataSize = contents.size() - 1; CHECK(HIERARCHY_ERROR == rc_replica_truncate(&comm, &truncate_doi, &output_str)); } + + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + const auto expected_json_output = + nlohmann::json{{"message", fmt::format(error_message_template, target_object.c_str(), expected_error_code)}, + {"replica_number", nlohmann::json::value_t::null}, + {"resource_hierarchy", nlohmann::json::value_t::null}}; + + CHECK(expected_json_output == json_out); } // Close the open data object so that it is back at rest. @@ -716,14 +970,40 @@ TEST_CASE("inputs_that_will_not_work") SECTION("DEST_RESC_NAME_KW_not_allowed") { - addKeyVal(&truncate_doi.condInput, DEST_RESC_NAME_KW, default_resc.c_str()); + constexpr const char* keyword = DEST_RESC_NAME_KW; + addKeyVal(&truncate_doi.condInput, keyword, default_resc.c_str()); CHECK(SYS_INVALID_INPUT_PARAM == rc_replica_truncate(&comm, &truncate_doi, &output_str)); + + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + const auto expected_json_output = + nlohmann::json{{"message", fmt::format("rs_replica_truncate: [{}] keyword not supported.", keyword)}, + {"replica_number", nlohmann::json::value_t::null}, + {"resource_hierarchy", nlohmann::json::value_t::null}}; + + CHECK(expected_json_output == json_out); } SECTION("DEST_RESC_HIER_STR_KW_not_allowed") { - addKeyVal(&truncate_doi.condInput, DEST_RESC_HIER_STR_KW, default_resc.c_str()); + constexpr const char* keyword = DEST_RESC_HIER_STR_KW; + addKeyVal(&truncate_doi.condInput, keyword, default_resc.c_str()); CHECK(SYS_INVALID_INPUT_PARAM == rc_replica_truncate(&comm, &truncate_doi, &output_str)); + + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + const auto expected_json_output = + nlohmann::json{{"message", fmt::format("rs_replica_truncate: [{}] keyword not supported.", keyword)}, + {"replica_number", nlohmann::json::value_t::null}, + {"resource_hierarchy", nlohmann::json::value_t::null}}; + + CHECK(expected_json_output == json_out); } SECTION("REPL_NUM_KW_and_RESC_NAME_KW_not_allowed_together") @@ -731,32 +1011,117 @@ TEST_CASE("inputs_that_will_not_work") addKeyVal(&truncate_doi.condInput, RESC_NAME_KW, test_resc.c_str()); addKeyVal(&truncate_doi.condInput, REPL_NUM_KW, "1"); CHECK(SYS_INVALID_INPUT_PARAM == rc_replica_truncate(&comm, &truncate_doi, &output_str)); + + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + const auto error_message = fmt::format( + "rs_replica_truncate: [{}] and [{}] keywords cannot be used together.", RESC_NAME_KW, REPL_NUM_KW); + + const auto expected_json_output = nlohmann::json{{"message", error_message}, + {"replica_number", nlohmann::json::value_t::null}, + {"resource_hierarchy", nlohmann::json::value_t::null}}; + + CHECK(expected_json_output == json_out); } SECTION("replica_number_that_does_not_exist") { + constexpr auto expected_error_code = SYS_REPLICA_DOES_NOT_EXIST; addKeyVal(&truncate_doi.condInput, REPL_NUM_KW, "1"); - CHECK(SYS_REPLICA_DOES_NOT_EXIST == rc_replica_truncate(&comm, &truncate_doi, &output_str)); + CHECK(expected_error_code == rc_replica_truncate(&comm, &truncate_doi, &output_str)); + + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + // There should be no replica information here because the expected failure occurs in such a way that no replica + // information is retrieved. After all, the replica being targeted does not exist. + const auto error_message = + fmt::format("rs_replica_truncate: Error occurred resolving hierarchy or getting information for [{}]: {}", + target_object.c_str(), + expected_error_code); + const auto expected_json_output = nlohmann::json{{"message", error_message}, + {"replica_number", nlohmann::json::value_t::null}, + {"resource_hierarchy", nlohmann::json::value_t::null}}; + + CHECK(expected_json_output == json_out); } SECTION("resource_name_with_no_replica") { - addKeyVal(&truncate_doi.condInput, RESC_HIER_STR_KW, test_resc.c_str()); - CHECK(SYS_REPLICA_DOES_NOT_EXIST == rc_replica_truncate(&comm, &truncate_doi, &output_str)); + constexpr auto expected_error_code = SYS_REPLICA_INACCESSIBLE; + addKeyVal(&truncate_doi.condInput, RESC_NAME_KW, test_resc.c_str()); + CHECK(expected_error_code == rc_replica_truncate(&comm, &truncate_doi, &output_str)); + + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + // There should be no replica information here because the expected failure indicates that the replica we wish + // to truncate does not exist. Even though an existing replica may have won the vote, it was not the intended + // target of the truncation and has nothing to do with the failure. Therefore, we expect no replica information. + const auto error_message = fmt::format("rs_replica_truncate: Hierarchy descending from specified resource name " + "[{}] does not have a replica of [{}] " + "or the replica is inaccessible at this time.", + test_resc.c_str(), + target_object.c_str()); + const auto expected_json_output = nlohmann::json{{"message", error_message}, + {"replica_number", nlohmann::json::value_t::null}, + {"resource_hierarchy", nlohmann::json::value_t::null}}; + + CHECK(expected_json_output == json_out); } SECTION("hierarchy_with_no_replica") { addKeyVal(&truncate_doi.condInput, RESC_HIER_STR_KW, test_resc.c_str()); CHECK(SYS_REPLICA_DOES_NOT_EXIST == rc_replica_truncate(&comm, &truncate_doi, &output_str)); + + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + // There should be no replica information here because the RESC_HIER_STR_KW bypasses hierarchy resolution and + // the expected failure occurs in such a way that no replica information is retrieved. After all, the replica + // being targeted does not exist. + const auto error_message = fmt::format("rs_replica_truncate: [{}] has no replica on resolved hierarchy [{}].", + target_object.c_str(), + test_resc.c_str()); + const auto expected_json_output = nlohmann::json{{"message", error_message}, + {"replica_number", nlohmann::json::value_t::null}, + {"resource_hierarchy", nlohmann::json::value_t::null}}; + + CHECK(expected_json_output == json_out); } SECTION("negative_dataSize") { truncate_doi.dataSize = -1; - const auto ec = irods::experimental::make_error_code(rc_replica_truncate(&comm, &truncate_doi, &output_str)); + const auto returned_error_code = rc_replica_truncate(&comm, &truncate_doi, &output_str); + const auto ec = irods::experimental::make_error_code(returned_error_code); CHECK(UNIX_FILE_TRUNCATE_ERR == irods::experimental::get_irods_error_code(ec)); CHECK(EINVAL == irods::experimental::get_errno(ec)); + + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + const auto error_message = + fmt::format("rs_replica_truncate: Error occurred while truncating replica of [{}] on [{}]: {}", + target_object.c_str(), + default_resc.c_str(), + returned_error_code); + const auto expected_json_output = + nlohmann::json{{"message", error_message}, {"replica_number", 0}, {"resource_hierarchy", default_resc}}; + + CHECK(expected_json_output == json_out); } // Ensure that the object was not updated on either replica. @@ -778,27 +1143,93 @@ TEST_CASE("really_bad_inputs") SECTION("nullptr_comm") { REQUIRE(SYS_INVALID_INPUT_PARAM == rc_replica_truncate(nullptr, nullptr, nullptr)); + + // This call does not even reach the server, so output_str is never assigned a value. + CHECK(nullptr == output_str); } SECTION("nullptr_input_struct") { REQUIRE(SYS_INVALID_INPUT_PARAM == rc_replica_truncate(&comm, nullptr, nullptr)); + + // This call does not even reach the server, so output_str is never assigned a value. + CHECK(nullptr == output_str); } SECTION("nullptr_output_pointer") { REQUIRE(SYS_INVALID_INPUT_PARAM == rc_replica_truncate(&comm, &truncate_doi, nullptr)); + + // This call does not even reach the server, so output_str is never assigned a value. + CHECK(nullptr == output_str); } SECTION("empty_input_struct") { - REQUIRE(OBJ_PATH_DOES_NOT_EXIST == rc_replica_truncate(&comm, &truncate_doi, &output_str)); + constexpr auto expected_error_code = OBJ_PATH_DOES_NOT_EXIST; + REQUIRE(expected_error_code == rc_replica_truncate(&comm, &truncate_doi, &output_str)); + + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + const auto error_message = + fmt::format("rs_replica_truncate: Error occurred resolving hierarchy or getting information for []: {}", + expected_error_code); + const auto expected_json_output = nlohmann::json{{"message", error_message}, + {"replica_number", nlohmann::json::value_t::null}, + {"resource_hierarchy", nlohmann::json::value_t::null}}; + + CHECK(expected_json_output == json_out); } SECTION("not_absolute_logical_path") { + constexpr auto expected_error_code = SYS_INVALID_FILE_PATH; std::strncpy(truncate_doi.objPath, "not_absolute_path", MAX_NAME_LEN); - REQUIRE(SYS_INVALID_FILE_PATH == rc_replica_truncate(&comm, &truncate_doi, &output_str)); + REQUIRE(expected_error_code == rc_replica_truncate(&comm, &truncate_doi, &output_str)); + + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + const auto error_message = + fmt::format("rs_replica_truncate: Error occurred resolving hierarchy or getting information for [{}]: {}", + truncate_doi.objPath, + expected_error_code); + const auto expected_json_output = nlohmann::json{{"message", error_message}, + {"replica_number", nlohmann::json::value_t::null}, + {"resource_hierarchy", nlohmann::json::value_t::null}}; + + CHECK(expected_json_output == json_out); + } + + SECTION("logical_path_with_nonexistent_zone") + { + constexpr const char* nonexistent_zone_name = "fakeZone"; + REQUIRE(!adm::client::zone_exists(comm, std::string_view{nonexistent_zone_name})); + + const auto target_object = fmt::format("/{}/home/rods/foo", nonexistent_zone_name); + constexpr auto expected_error_code = OBJ_PATH_DOES_NOT_EXIST; + std::strncpy(truncate_doi.objPath, target_object.c_str(), MAX_NAME_LEN); + REQUIRE(expected_error_code == rc_replica_truncate(&comm, &truncate_doi, &output_str)); + + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + + const auto error_message = + fmt::format("rs_replica_truncate: Error occurred resolving hierarchy or getting information for [{}]: {}", + target_object.c_str(), + expected_error_code); + const auto expected_json_output = nlohmann::json{{"message", error_message}, + {"replica_number", nlohmann::json::value_t::null}, + {"resource_hierarchy", nlohmann::json::value_t::null}}; + + CHECK(expected_json_output == json_out); } } // really_bad_inputs @@ -824,6 +1255,7 @@ TEST_CASE("checksum_tests") REQUIRE(fs::client::remove_all(comm, sandbox, fs::remove_options::no_trash)); }}; + const std::string default_resc = "demoResc"; const auto target_object = sandbox / "target_object"; static constexpr auto contents = std::string_view{"content!"}; @@ -861,6 +1293,15 @@ TEST_CASE("checksum_tests") const auto new_checksum = replica::replica_checksum(comm, target_object, 0, replica::verification_calculation::always); CHECK(original_checksum == new_checksum); + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + const auto expected_json_output = nlohmann::json{ + {"message", fmt::format(same_size_message, target_object.c_str(), default_resc, contents.size())}, + {"replica_number", 0}, + {"resource_hierarchy", default_resc.c_str()}}; + CHECK(expected_json_output == json_out); } SECTION("larger_size") { @@ -872,6 +1313,13 @@ TEST_CASE("checksum_tests") const auto new_checksum = replica::replica_checksum(comm, target_object, 0, replica::verification_calculation::always); CHECK(original_checksum != new_checksum); + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + const auto expected_json_output = + nlohmann::json{{"message", ""}, {"replica_number", 0}, {"resource_hierarchy", default_resc.c_str()}}; + CHECK(expected_json_output == json_out); } SECTION("smaller_size") { @@ -883,6 +1331,13 @@ TEST_CASE("checksum_tests") const auto new_checksum = replica::replica_checksum(comm, target_object, 0, replica::verification_calculation::always); CHECK(original_checksum != new_checksum); + // Ensure that the returned output structure has the expected contents. + REQUIRE(nullptr != output_str); + nlohmann::json json_out; + REQUIRE_NOTHROW([&] { json_out = nlohmann::json::parse(output_str); }()); + const auto expected_json_output = + nlohmann::json{{"message", ""}, {"replica_number", 0}, {"resource_hierarchy", default_resc.c_str()}}; + CHECK(expected_json_output == json_out); } }