diff --git a/CHANGELOG.md b/CHANGELOG.md index 4978c33fbef..201f1a75d84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ ----------- ### Internals -* None. +* Added a CombinedTests target that runs tests from object-store, sync, and core together. ([PR #6964](https://github.com/realm/realm-core/pull/6964)) ---------------------------------------------- diff --git a/Jenkinsfile b/Jenkinsfile index 59aaa0db17c..15f22a5b311 100755 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -668,32 +668,19 @@ def doBuildApplePlatform(String platform, String buildType, boolean test = false if (test) { dir('build-xcode-platforms') { if (platform != 'iphonesimulator') error 'Testing is only available for iOS Simulator' - sh "xcodebuild -scheme CoreTests -configuration ${buildType} -sdk iphonesimulator -arch x86_64" - // sh "xcodebuild -scheme SyncTests -configuration ${buildType} -sdk iphonesimulator -arch x86_64 IPHONEOS_DEPLOYMENT_TARGET=13" - sh "xcodebuild -scheme ObjectStoreTests -configuration ${buildType} -sdk iphonesimulator -arch x86_64 IPHONEOS_DEPLOYMENT_TARGET=13" + sh "xcodebuild -scheme CombinedTests -configuration ${buildType} -sdk iphonesimulator -arch x86_64" def env = environment().collect { v -> "SIMCTL_CHILD_${v}" } - def resultFile = "${WORKSPACE}/core-test-report.xml" + def resultFile = "${WORKSPACE}/combined-test-report.xml" withEnv(env + ["SIMCTL_CHILD_UNITTEST_XML=${resultFile}", "SIMCTL_CHILD_UNITTEST_SUITE_NAME=iOS-${buildType}-Core"]) { - sh "$WORKSPACE/tools/run-in-simulator.sh 'test/${buildType}-${platform}/realm-tests.app' 'io.realm.CoreTests' '${resultFile}'" - } - // Sync tests currently don't work on iOS because they require an unimplemented server feature - // resultFile = "${WORKSPACE}/sync-test-report.xml" - // withEnv(env + ["SIMCTL_CHILD_UNITTEST_XML=${resultFile}", "SIMCTL_CHILD_UNITTEST_SUITE_NAME=iOS-${buildType}-Sync"]) { - // sh "$WORKSPACE/tools/run-in-simulator.sh 'test/${buildType}-${platform}/realm-sync-tests.app' 'io.realm.SyncTests' '${resultFile}'" - // } - resultFile = "${WORKSPACE}/object-store-test-report.xml" - withEnv(env + ["SIMCTL_CHILD_UNITTEST_XML=${resultFile}", "SIMCTL_CHILD_UNITTEST_SUITE_NAME=iOS-${buildType}-Object-Store"]) { - sh "$WORKSPACE/tools/run-in-simulator.sh 'test/object-store/${buildType}-${platform}/realm-object-store-tests.app' 'io.realm.ObjectStoreTests' '${resultFile}'" + sh "$WORKSPACE/tools/run-in-simulator.sh 'test/${buildType}-${platform}/realm-combined-tests.app' 'io.realm.CombinedTests' '${resultFile}'" } } } } if (test) { - junit testResults: 'core-test-report.xml' - // junit testResults: 'sync-test-report.xml' - junit testResults: 'object-store-test-report.xml' + junit testResults: 'combined-test-report.xml' } String tarball = "realm-${buildType}-${gitDescribeVersion}-${platform}-devel.tar.gz"; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index dcc4e990352..97df6f482f9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -3,7 +3,7 @@ add_custom_target(benchmarks) add_subdirectory(object-store) # AFL and LIBFUZZER not yet supported by Windows -if(NOT CMAKE_SYSTEM_NAME MATCHES "^Windows" AND NOT EMSCRIPTEN) +if(NOT CMAKE_SYSTEM_NAME MATCHES "^Windows" AND NOT EMSCRIPTEN AND NOT ANDROID) add_subdirectory(fuzzy) add_subdirectory(realm-fuzzer) endif() @@ -23,7 +23,6 @@ set(CORE_TEST_SOURCES test_lang_bind_helper.cpp test_parser.cpp - test_all.cpp test_alloc.cpp test_array.cpp test_array_blob.cpp @@ -126,8 +125,6 @@ set_source_files_properties(test_query_geo.cpp PROPERTIES COMPILE_FLAGS "$<$: -Wno-unknown-pragmas>" ) -# FIXME: Benchmarks - if (MSVC) append_source_file_compile_options( FILES test_parser.cpp test_table.cpp test_query.cpp test_query_big.cpp @@ -136,12 +133,17 @@ if (MSVC) endif() # Resources required for running the tests -file(GLOB REQUIRED_TEST_FILES RELATIVE ${CMAKE_CURRENT_BINARY_DIR} +file(GLOB REQUIRED_TEST_FILES "*.json" "*.realm" "expect_string.txt") -add_executable(CoreTests main.cpp ${CORE_TESTS} ${REQUIRED_TEST_FILES} ${REALM_TEST_HEADERS}) +add_library(CoreTestLib OBJECT ${CORE_TESTS} ${REQUIRED_TEST_FILES} ${REALM_TEST_HEADERS}) +enable_stdfilesystem(CoreTestLib) +target_link_libraries(CoreTestLib QueryParser) + +add_executable(CoreTests main.cpp test_all.cpp ${REQUIRED_TEST_FILES}) +target_link_libraries(CoreTests CoreTestLib TestUtil) set_target_resources(CoreTests "${REQUIRED_TEST_FILES}") set_target_properties(CoreTests PROPERTIES OUTPUT_NAME "realm-tests" @@ -167,7 +169,6 @@ if(WINDOWS_STORE) set_property(SOURCE ${UWP_ASSETS} PROPERTY VS_DEPLOYMENT_LOCATION "Assets") endif() -target_link_libraries(CoreTests TestUtil QueryParser) enable_stdfilesystem(CoreTests) if(UNIX AND NOT APPLE) @@ -179,9 +180,9 @@ target_include_directories(CoreTests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") add_bundled_test(CoreTests) +# SyncTests if(REALM_ENABLE_SYNC) set(SYNC_TESTS - test_all.cpp test_array_sync.cpp test_changeset_encoding.cpp test_client_reset.cpp @@ -210,7 +211,7 @@ if(REALM_ENABLE_SYNC) test_util_websocket.cpp ) - set(TEST_HEADERS + set(SYNC_TEST_HEADERS test.hpp test_all.hpp fuzz_tester.hpp @@ -218,19 +219,7 @@ if(REALM_ENABLE_SYNC) sync_fixtures.hpp ) - set(TEST_CLIENT_SOURCES - client/main.cpp - client/peer.cpp - ) - - set(TEST_CLIENT_HEADERS - client/metrics.hpp - client/peer.hpp - client/object_observer.hpp - client/statistics.hpp - ) - - file(GLOB TEST_RESOURCES RELATIVE ${CMAKE_CURRENT_BINARY_DIR} + file(GLOB SYNC_TEST_RESOURCES *.json *.pem ../certificate-authority/certs/* ../certificate-authority/root-ca/*.pem @@ -243,12 +232,15 @@ if(REALM_ENABLE_SYNC) list(APPEND SYNC_TESTS test.manifest) endif() - add_executable(SyncTests main.cpp ${SYNC_TESTS} ${TEST_HEADERS} ${TEST_RESOURCES}) + add_library(SyncTestLib OBJECT ${SYNC_TESTS} ${SYNC_TEST_HEADERS} ${SYNC_TEST_RESOURCES}) + enable_stdfilesystem(SyncTestLib) + target_link_libraries(SyncTestLib Sync SyncServer Storage) + + add_executable(SyncTests main.cpp test_all.cpp ${SYNC_TEST_RESOURCES}) set_target_properties(SyncTests PROPERTIES OUTPUT_NAME "realm-sync-tests") - set_target_resources(SyncTests "${TEST_RESOURCES}") + set_target_resources(SyncTests "${SYNC_TEST_RESOURCES}") enable_stdfilesystem(SyncTests) - # Sync lib is included with SyncServer - target_link_libraries(SyncTests TestUtil SyncServer) + target_link_libraries(SyncTests SyncTestLib TestUtil) add_bundled_test(SyncTests) if(UNIX AND NOT APPLE) @@ -256,3 +248,33 @@ if(REALM_ENABLE_SYNC) target_link_libraries(SyncTests "-rdynamic") endif() endif() + +# CombinedTests +get_directory_property(OS_TEST_RESOURCES DIRECTORY object-store TEST_RESOURCES) +set(COMBINED_RESOURCES "${REQUIRED_TEST_FILES};${SYNC_TEST_RESOURCES};${OS_TEST_RESOURCES}") + +add_executable(CombinedTests combined_tests.cpp test_all.cpp ${COMBINED_RESOURCES}) +set_target_resources(CombinedTests "${COMBINED_RESOURCES}") +set_target_properties(CombinedTests PROPERTIES + OUTPUT_NAME "realm-combined-tests" + VS_WINDOWS_TARGET_PLATFORM_MIN_VERSION 10.0.17134.0 +) +target_include_directories(CombinedTests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}" "object-store") + +if(WINDOWS_STORE) + target_sources(CombinedTests PRIVATE ${UWP_SOURCES}) +endif() + +if(REALM_ENABLE_SYNC) + target_link_libraries(CombinedTests ObjectStoreTestLib CoreTestLib SyncTestLib TestUtil) +else() + target_link_libraries(CombinedTests ObjectStoreTestLib CoreTestLib TestUtil) +endif() + +enable_stdfilesystem(CombinedTests) + +if(UNIX AND NOT APPLE) + # This enables symbols in backtraces + target_link_libraries(CombinedTests "-rdynamic") +endif() + diff --git a/test/combined_tests.cpp b/test/combined_tests.cpp new file mode 100644 index 00000000000..9673e9930b9 --- /dev/null +++ b/test/combined_tests.cpp @@ -0,0 +1,38 @@ +/************************************************************************* + * + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + **************************************************************************/ + +#include "test_all.hpp" +#include "util/test_path.hpp" + +#include + +// see test/object-store/test_runner.cpp +extern int run_object_store_tests(int, const char**); + +int main(int argc, const char* argv[]) +{ + if (!realm::test_util::initialize_test_path(argc, argv)) + return 1; + int status = test_all(); + if (status) { + std::cerr << "core and sync tests failed: " << status << std::endl; + return status; + } + std::cout << "core and sync tests passed\n"; + return run_object_store_tests(argc, argv); +} diff --git a/test/object-store/CMakeLists.txt b/test/object-store/CMakeLists.txt index d0a845cbabd..d29e960c645 100644 --- a/test/object-store/CMakeLists.txt +++ b/test/object-store/CMakeLists.txt @@ -7,13 +7,15 @@ set(HEADERS ) set(SOURCES + # slowest to compile first + set.cpp + backup.cpp collection_change_indices.cpp dictionary.cpp frozen_objects.cpp index_set.cpp list.cpp - main.cpp migrations.cpp object.cpp object_store.cpp @@ -22,12 +24,12 @@ set(SOURCES results.cpp schema.cpp sectioned_results.cpp - set.cpp + test_runner.cpp thread_safe_reference.cpp transaction_log_parsing.cpp uuid.cpp c_api/c_api.cpp - c_api/c_api.c + c_api/c_api_file_tests.c util/event_loop.cpp util/test_file.cpp @@ -38,9 +40,7 @@ if (REALM_ENABLE_GEOSPATIAL) list(APPEND SOURCES geospatial.cpp) endif() -file(GLOB RESOURCES RELATIVE ${CMAKE_CURRENT_BINARY_DIR} - ../*.pem - *.realm) +file(GLOB RESOURCES "*.realm" "../*.pem") if(REALM_ENABLE_AUTH_TESTS) list(APPEND SOURCES util/sync/baas_admin_api.cpp) @@ -78,16 +78,22 @@ if(REALM_ENABLE_SYNC) endif() endif() -add_executable(ObjectStoreTests ${SOURCES} ${HEADERS} ${RESOURCES}) -set_target_properties(ObjectStoreTests PROPERTIES OUTPUT_NAME realm-object-store-tests) -set_target_resources(ObjectStoreTests "${RESOURCES}") +set_property(DIRECTORY PROPERTY TEST_RESOURCES "${RESOURCES}") + +add_library(ObjectStoreTestLib OBJECT ${SOURCES} ${HEADERS} ${RESOURCES}) if(MSVC) # increase the number of sections supported in an obj file for the heavily templated tests - target_compile_options(ObjectStoreTests PRIVATE /bigobj) + target_compile_options(ObjectStoreTestLib PRIVATE /bigobj) endif() -target_link_libraries(ObjectStoreTests Catch2::Catch2 ObjectStore TestUtil RealmFFIStatic) +target_link_libraries(ObjectStoreTestLib Catch2::Catch2 ObjectStore RealmFFIStatic TestUtil) +enable_stdfilesystem(ObjectStoreTestLib) + +add_executable(ObjectStoreTests main.cpp ${RESOURCES}) +set_target_properties(ObjectStoreTests PROPERTIES OUTPUT_NAME realm-object-store-tests) +target_link_libraries(ObjectStoreTests ObjectStoreTestLib TestUtil) +set_target_resources(ObjectStoreTests "${RESOURCES}") enable_stdfilesystem(ObjectStoreTests) create_coverage_target(generate-coverage ObjectStoreTests) @@ -120,7 +126,7 @@ if(REALM_ENABLE_SYNC) endif() if(REALM_ENABLE_SYNC) - target_link_libraries(ObjectStoreTests SyncServer) + target_link_libraries(ObjectStoreTestLib SyncServer) option(REALM_ENABLE_AUTH_TESTS "" OFF) if(REALM_ENABLE_AUTH_TESTS) if(NOT REALM_MONGODB_ENDPOINT) @@ -128,7 +134,7 @@ if(REALM_ENABLE_SYNC) endif() message(STATUS "Auth tests enabled: ${REALM_MONGODB_ENDPOINT}") - target_compile_definitions(ObjectStoreTests PRIVATE + target_compile_definitions(ObjectStoreTestLib PRIVATE REALM_ENABLE_AUTH_TESTS=1 REALM_MONGODB_ENDPOINT="${REALM_MONGODB_ENDPOINT}" ) @@ -141,18 +147,18 @@ if(REALM_ENABLE_SYNC) endif() find_package(CURL REQUIRED) - target_link_libraries(ObjectStoreTests CURL::libcurl) + target_link_libraries(ObjectStoreTestLib CURL::libcurl) endif() endif() if(REALM_TEST_LOGGING) - target_compile_definitions(ObjectStoreTests PRIVATE + target_compile_definitions(ObjectStoreTestLib PRIVATE TEST_ENABLE_LOGGING=1 ) if(REALM_TEST_LOGGING_LEVEL) message(STATUS "Test logging level: ${REALM_TEST_LOGGING_LEVEL}") - target_compile_definitions(ObjectStoreTests PRIVATE + target_compile_definitions(ObjectStoreTestLib PRIVATE TEST_LOGGING_LEVEL=${REALM_TEST_LOGGING_LEVEL} ) else() @@ -168,7 +174,7 @@ if(REALM_TEST_TIMEOUT_EXTRA) message(STATUS "Test wait timeouts extended by ${REALM_TEST_TIMEOUT_EXTRA} seconds") endif() -target_include_directories(ObjectStoreTests PRIVATE +target_include_directories(ObjectStoreTestLib PRIVATE ${CATCH_INCLUDE_DIR} ${JSON_INCLUDE_DIR} ${CMAKE_CURRENT_SOURCE_DIR} @@ -177,7 +183,7 @@ target_include_directories(ObjectStoreTests PRIVATE # on Apple platforms we use the built-in CFRunLoop # on WebAssembly we use an Emscripten-specific Scheduler and runloop # everywhere else it's libuv, except UWP where it doesn't build -if(NOT APPLE AND NOT EMSCRIPTEN AND NOT WINDOWS_STORE) +if(NOT APPLE AND NOT EMSCRIPTEN AND NOT WINDOWS_STORE AND NOT ANDROID) option(REALM_FETCH_MISSING_DEPENDENCIES "Download missing dependencies with CMake's FetchContent where possible" ON) if(REALM_FETCH_MISSING_DEPENDENCIES) find_package(LibUV) @@ -204,8 +210,8 @@ if(NOT APPLE AND NOT EMSCRIPTEN AND NOT WINDOWS_STORE) set(libuv_target uv_a) endif() - target_link_libraries(ObjectStoreTests ${libuv_target}) - target_compile_definitions(ObjectStoreTests PRIVATE TEST_SCHEDULER_UV=1) + target_link_libraries(ObjectStoreTestLib ${libuv_target}) + target_compile_definitions(ObjectStoreTestLib PRIVATE TEST_SCHEDULER_UV=1) if (MSVC) get_target_property(comp_opts ${libuv_target} COMPILE_OPTIONS) @@ -216,6 +222,6 @@ endif() add_subdirectory(notifications-fuzzer) -if(NOT EMSCRIPTEN AND NOT WINDOWS_STORE) +if(NOT EMSCRIPTEN AND NOT WINDOWS_STORE AND NOT ANDROID) add_subdirectory(benchmarks) endif() diff --git a/test/object-store/benchmarks/CMakeLists.txt b/test/object-store/benchmarks/CMakeLists.txt index d77f5f021b4..28f24771c33 100644 --- a/test/object-store/benchmarks/CMakeLists.txt +++ b/test/object-store/benchmarks/CMakeLists.txt @@ -43,7 +43,7 @@ add_dependencies(benchmarks object-store-benchmarks) # on Apple platforms we use the built-in CFRunLoop # everywhere else it's libuv, except UWP where it doesn't build -if(NOT APPLE AND NOT WINDOWS_STORE AND NOT EMSCRIPTEN) +if(NOT APPLE AND NOT WINDOWS_STORE AND NOT EMSCRIPTEN AND NOT ANDROID) # libuv_target is defined in the parent CMakeLists.txt file target_link_libraries(object-store-benchmarks ${libuv_target}) target_compile_definitions(object-store-benchmarks PRIVATE TEST_SCHEDULER_UV=1) diff --git a/test/object-store/c_api/c_api.c b/test/object-store/c_api/c_api_file_tests.c similarity index 100% rename from test/object-store/c_api/c_api.c rename to test/object-store/c_api/c_api_file_tests.c diff --git a/test/object-store/main.cpp b/test/object-store/main.cpp index 630eb4e4c8d..f8c849e6bdd 100644 --- a/test/object-store/main.cpp +++ b/test/object-store/main.cpp @@ -1,6 +1,6 @@ //////////////////////////////////////////////////////////////////////////// // -// Copyright 2016 Realm Inc. +// Copyright 2023 Realm Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,229 +16,10 @@ // //////////////////////////////////////////////////////////////////////////// -#include -#include - -#include -#include - -#if TEST_SCHEDULER_UV -#include -#endif - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -using namespace std::chrono; +// see test_runner.cpp +extern int run_object_store_tests(int, const char**); int main(int argc, const char** argv) { - auto t1 = steady_clock::now(); - - realm::test_util::initialize_test_path(1, argv); - - Catch::ConfigData config; - - if (const char* str = getenv("UNITTEST_EVERGREEN_TEST_RESULTS"); str && strlen(str) != 0) { - std::cout << "Configuring evergreen reporter to store test results in " << str << std::endl; - // If the output file already exists, make a copy so these results can be appended to it - std::map custom_options; - if (std::filesystem::exists(str)) { - std::string results_copy = realm::util::format("%1.bak", str); - std::filesystem::copy(str, results_copy, std::filesystem::copy_options::overwrite_existing); - custom_options["json_file"] = results_copy; - std::cout << "Existing results file copied to " << results_copy << std::endl; - } - config.showDurations = Catch::ShowDurations::Always; // this is to help debug hangs in Evergreen - config.reporterSpecifications.push_back(Catch::ReporterSpec{"console", {}, {}, {}}); - config.reporterSpecifications.push_back( - Catch::ReporterSpec{"evergreen", {str}, {}, std::move(custom_options)}); - } - else if (const char* str = getenv("UNITTEST_XML"); str && strlen(str) != 0) { - std::cout << "Configuring jUnit reporter to store test results in " << str << std::endl; - config.showDurations = Catch::ShowDurations::Always; // this is to help debug hangs in Jenkins - config.reporterSpecifications.push_back(Catch::ReporterSpec{"console", {}, {}, {}}); - config.reporterSpecifications.push_back(Catch::ReporterSpec{"junit", {str}, {}, {}}); - } - - if (const char* env = getenv("UNITTEST_ENCRYPT_ALL")) { - std::string str(env); - for (auto& c : str) { - c = tolower(c); - } - if (str == "1" || str == "on" || str == "yes") { - realm::test_util::enable_always_encrypt(); - } - } - -#ifdef TEST_TIMEOUT_EXTRA - std::cout << "Test wait timeouts extended by " << TEST_TIMEOUT_EXTRA << " seconds" << std::endl; -#endif - -#if TEST_SCHEDULER_UV - realm::util::Scheduler::set_default_factory([]() { - return std::make_shared(); - }); -#endif - - Catch::Session session; - session.useConfigData(config); - int result = session.run(argc, argv); - - auto t2 = steady_clock::now(); - auto ms_int = duration_cast(t2 - t1); - std::cout << "Test time: " << (ms_int.count() / 1000.0) << "s" << std::endl << std::endl; - return result < 0xff ? result : 0xff; + return run_object_store_tests(argc, argv); } - -namespace Catch { -class EvergreenReporter : public CumulativeReporterBase { -public: - struct TestResult { - TestResult() - : start_time{std::chrono::system_clock::now()} - , end_time{} - , status{"unknown"} - { - } - std::chrono::system_clock::time_point start_time; - std::chrono::system_clock::time_point end_time; - std::string status; - }; - - using Base = CumulativeReporterBase; - using CumulativeReporterBase::CumulativeReporterBase; - static std::string getDescription() - { - return "Reports test results in a format consumable by Evergreen."; - } - - void assertionEnded(AssertionStats const& assertionStats) override - { - if (!assertionStats.assertionResult.isOk()) { - std::cerr << "Assertion failure: " << assertionStats.assertionResult.getSourceInfo() << std::endl; - std::cerr << "\t from expresion: '" << assertionStats.assertionResult.getExpression() << "'" << std::endl; - std::cerr << "\t with expansion: '" << assertionStats.assertionResult.getExpandedExpression() << "'" - << std::endl; - for (auto& message : assertionStats.infoMessages) { - std::cerr << "\t message: " << message.message << std::endl; - } - std::cerr << std::endl; - } - } - void testCaseStarting(TestCaseInfo const& testCaseInfo) override - { - m_results.emplace(std::make_pair(testCaseInfo.name, TestResult{})); - Base::testCaseStarting(testCaseInfo); - } - void testCaseEnded(TestCaseStats const& testCaseStats) override - { - auto it = m_results.find(testCaseStats.testInfo->name); - if (it == m_results.end()) { - throw std::runtime_error("logic error in Evergreen section reporter, could not end test case '" + - testCaseStats.testInfo->name + "' which was never tracked as started."); - } - if (testCaseStats.totals.assertions.allPassed()) { - it->second.status = "pass"; - } - else { - it->second.status = "fail"; - } - it->second.end_time = std::chrono::system_clock::now(); - Base::testCaseEnded(testCaseStats); - } - void sectionStarting(SectionInfo const& sectionInfo) override - { - if (m_pending_name.empty()) { - m_pending_name = sectionInfo.name; - } - else { - m_pending_name += "::" + sectionInfo.name; - } - m_pending_test = {}; - Base::sectionStarting(sectionInfo); - } - void sectionEnded(SectionStats const& sectionStats) override - { - if (!m_pending_name.empty()) { - if (sectionStats.assertions.allPassed()) { - m_pending_test.status = "pass"; - } - else { - m_pending_test.status = "fail"; - } - m_pending_test.end_time = std::chrono::system_clock::now(); - m_results.emplace(std::make_pair(m_pending_name, m_pending_test)); - m_pending_name = ""; - } - Base::sectionEnded(sectionStats); - } - void testRunEndedCumulative() override - { - auto& options = m_customOptions; - auto& json_file = options["json_file"]; - nlohmann::json results_arr = nlohmann::json::array(); - // If the results file already exists, include the results from that file - try { - if (!json_file.empty() && std::filesystem::exists(json_file)) { - std::ifstream f(json_file); - // Make sure the file was successfully opened and is not empty - if (f.is_open() && !f.eof()) { - nlohmann::json existing_data = nlohmann::json::parse(f); - auto results = existing_data.find("results"); - if (results != existing_data.end() && results->is_array()) { - std::cout << "Appending tests from previous results" << std::endl; - results_arr = *results; - } - } - } - } - catch (nlohmann::json::exception&) { - // json parse error, ignore the entries - } - catch (std::exception&) { - // unable to open/read file - } - for (const auto& [test_name, cur_result] : m_results) { - auto to_millis = [](const auto& tp) -> double { - return static_cast( - std::chrono::duration_cast(tp.time_since_epoch()).count()); - }; - double start_secs = to_millis(cur_result.start_time) / 1000; - double end_secs = to_millis(cur_result.end_time) / 1000; - int exit_code = 0; - if (cur_result.status != "pass") { - exit_code = 1; - } - - nlohmann::json cur_result_obj = {{"test_file", test_name}, {"status", cur_result.status}, - {"exit_code", exit_code}, {"start", start_secs}, - {"end", end_secs}, {"elapsed", end_secs - start_secs}}; - results_arr.push_back(std::move(cur_result_obj)); - } - auto result_file_obj = nlohmann::json{{"results", std::move(results_arr)}}; - m_stream << result_file_obj << std::endl; - if (!json_file.empty() && std::filesystem::exists(json_file)) { - // Delete the old results file - std::filesystem::remove(json_file); - } - } - - TestResult m_pending_test; - std::string m_pending_name; - std::vector> m_current_stack; - std::map m_results; -}; - -CATCH_REGISTER_REPORTER("evergreen", EvergreenReporter) - -} // end namespace Catch diff --git a/test/object-store/sync/session/session.cpp b/test/object-store/sync/session/session.cpp index 7839507ff9e..61bac0da385 100644 --- a/test/object-store/sync/session/session.cpp +++ b/test/object-store/sync/session/session.cpp @@ -371,7 +371,8 @@ TEST_CASE("sync: error handling", "[sync][session]") { CHECK_THAT(error->status.reason(), Catch::Matchers::StartsWith("Failed to connect to sync: Host not found")); } -#ifndef SWIFT_PACKAGE // requires test resource files + // requires test resource files and a server implementation +#if !(defined(SWIFT_PACKAGE) || REALM_MOBILE) SECTION("reports TLS error as handshake failed") { TestSyncManager ssl_sync_manager({}, {StartImmediately{true}, EnableSSL{true}}); auto app = ssl_sync_manager.app(); @@ -393,7 +394,7 @@ TEST_CASE("sync: error handling", "[sync][session]") { Catch::Matchers::StartsWith("TLS handshake failed: OpenSSL error: certificate verify failed")); #endif } -#endif +#endif // !defined(SWIFT_PACKAGE) && !REALM_MOBILE using ProtocolError = realm::sync::ProtocolError; using ProtocolErrorInfo = realm::sync::ProtocolErrorInfo; diff --git a/test/object-store/test_runner.cpp b/test/object-store/test_runner.cpp new file mode 100644 index 00000000000..dce3b7734ac --- /dev/null +++ b/test/object-store/test_runner.cpp @@ -0,0 +1,245 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include +#include + +#if TEST_SCHEDULER_UV +#include +#endif + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + + +int run_object_store_tests(int argc, const char** argv); + +int run_object_store_tests(int argc, const char** argv) +{ + auto t1 = std::chrono::steady_clock::now(); + + realm::test_util::initialize_test_path(1, argv); + + Catch::ConfigData config; + + if (const char* str = getenv("UNITTEST_EVERGREEN_TEST_RESULTS"); str && strlen(str) != 0) { + std::cout << "Configuring evergreen reporter to store test results in " << str << std::endl; + // If the output file already exists, make a copy so these results can be appended to it + std::map custom_options; + if (std::filesystem::exists(str)) { + std::string results_copy = realm::util::format("%1.bak", str); + std::filesystem::copy(str, results_copy, std::filesystem::copy_options::overwrite_existing); + custom_options["json_file"] = results_copy; + std::cout << "Existing results file copied to " << results_copy << std::endl; + } + config.showDurations = Catch::ShowDurations::Always; // this is to help debug hangs in Evergreen + config.reporterSpecifications.push_back(Catch::ReporterSpec{"console", {}, {}, {}}); + config.reporterSpecifications.push_back( + Catch::ReporterSpec{"evergreen", {str}, {}, std::move(custom_options)}); + } + else if (const char* str = getenv("UNITTEST_XML"); str && strlen(str) != 0) { + std::cout << "Configuring jUnit reporter to store test results in " << str << std::endl; + config.showDurations = Catch::ShowDurations::Always; // this is to help debug hangs in Jenkins + config.reporterSpecifications.push_back(Catch::ReporterSpec{"console", {}, {}, {}}); + config.reporterSpecifications.push_back(Catch::ReporterSpec{"junit", {str}, {}, {}}); + } + + if (const char* env = getenv("UNITTEST_ENCRYPT_ALL")) { + std::string str(env); + for (auto& c : str) { + c = tolower(c); + } + if (str == "1" || str == "on" || str == "yes") { + realm::test_util::enable_always_encrypt(); + } + } + +#ifdef TEST_TIMEOUT_EXTRA + std::cout << "Test wait timeouts extended by " << TEST_TIMEOUT_EXTRA << " seconds" << std::endl; +#endif + +#if TEST_SCHEDULER_UV + realm::util::Scheduler::set_default_factory([]() { + return std::make_shared(); + }); +#endif + + Catch::Session session; + session.useConfigData(config); + int result = session.run(argc, argv); + + auto t2 = std::chrono::steady_clock::now(); + auto ms_int = std::chrono::duration_cast(t2 - t1); + std::cout << "Test time: " << (ms_int.count() / 1000.0) << "s" << std::endl << std::endl; + return result < 0xff ? result : 0xff; +} + +namespace Catch { +class EvergreenReporter : public CumulativeReporterBase { +public: + struct TestResult { + TestResult() + : start_time{std::chrono::system_clock::now()} + , end_time{} + , status{"unknown"} + { + } + std::chrono::system_clock::time_point start_time; + std::chrono::system_clock::time_point end_time; + std::string status; + }; + + using Base = CumulativeReporterBase; + using CumulativeReporterBase::CumulativeReporterBase; + static std::string getDescription() + { + return "Reports test results in a format consumable by Evergreen."; + } + + void assertionEnded(AssertionStats const& assertionStats) override + { + if (!assertionStats.assertionResult.isOk()) { + std::cerr << "Assertion failure: " << assertionStats.assertionResult.getSourceInfo() << std::endl; + std::cerr << "\t from expresion: '" << assertionStats.assertionResult.getExpression() << "'" << std::endl; + std::cerr << "\t with expansion: '" << assertionStats.assertionResult.getExpandedExpression() << "'" + << std::endl; + for (auto& message : assertionStats.infoMessages) { + std::cerr << "\t message: " << message.message << std::endl; + } + std::cerr << std::endl; + } + } + void testCaseStarting(TestCaseInfo const& testCaseInfo) override + { + m_results.emplace(std::make_pair(testCaseInfo.name, TestResult{})); + Base::testCaseStarting(testCaseInfo); + } + void testCaseEnded(TestCaseStats const& testCaseStats) override + { + auto it = m_results.find(testCaseStats.testInfo->name); + if (it == m_results.end()) { + throw std::runtime_error("logic error in Evergreen section reporter, could not end test case '" + + testCaseStats.testInfo->name + "' which was never tracked as started."); + } + if (testCaseStats.totals.assertions.allPassed()) { + it->second.status = "pass"; + } + else { + it->second.status = "fail"; + } + it->second.end_time = std::chrono::system_clock::now(); + Base::testCaseEnded(testCaseStats); + } + void sectionStarting(SectionInfo const& sectionInfo) override + { + if (m_pending_name.empty()) { + m_pending_name = sectionInfo.name; + } + else { + m_pending_name += "::" + sectionInfo.name; + } + m_pending_test = {}; + Base::sectionStarting(sectionInfo); + } + void sectionEnded(SectionStats const& sectionStats) override + { + if (!m_pending_name.empty()) { + if (sectionStats.assertions.allPassed()) { + m_pending_test.status = "pass"; + } + else { + m_pending_test.status = "fail"; + } + m_pending_test.end_time = std::chrono::system_clock::now(); + m_results.emplace(std::make_pair(m_pending_name, m_pending_test)); + m_pending_name = ""; + } + Base::sectionEnded(sectionStats); + } + void testRunEndedCumulative() override + { + auto& options = m_customOptions; + auto& json_file = options["json_file"]; + nlohmann::json results_arr = nlohmann::json::array(); + // If the results file already exists, include the results from that file + try { + if (!json_file.empty() && std::filesystem::exists(json_file)) { + std::ifstream f(json_file); + // Make sure the file was successfully opened and is not empty + if (f.is_open() && !f.eof()) { + nlohmann::json existing_data = nlohmann::json::parse(f); + auto results = existing_data.find("results"); + if (results != existing_data.end() && results->is_array()) { + std::cout << "Appending tests from previous results" << std::endl; + results_arr = *results; + } + } + } + } + catch (nlohmann::json::exception&) { + // json parse error, ignore the entries + } + catch (std::exception&) { + // unable to open/read file + } + for (const auto& [test_name, cur_result] : m_results) { + auto to_millis = [](const auto& tp) -> double { + return static_cast( + std::chrono::duration_cast(tp.time_since_epoch()).count()); + }; + double start_secs = to_millis(cur_result.start_time) / 1000; + double end_secs = to_millis(cur_result.end_time) / 1000; + int exit_code = 0; + if (cur_result.status != "pass") { + exit_code = 1; + } + + nlohmann::json cur_result_obj = {{"test_file", test_name}, {"status", cur_result.status}, + {"exit_code", exit_code}, {"start", start_secs}, + {"end", end_secs}, {"elapsed", end_secs - start_secs}}; + results_arr.push_back(std::move(cur_result_obj)); + } + auto result_file_obj = nlohmann::json{{"results", std::move(results_arr)}}; + m_stream << result_file_obj << std::endl; + if (!json_file.empty() && std::filesystem::exists(json_file)) { + // Delete the old results file + std::filesystem::remove(json_file); + } + } + + TestResult m_pending_test; + std::string m_pending_name; + std::vector> m_current_stack; + std::map m_results; +}; + +CATCH_REGISTER_REPORTER("evergreen", EvergreenReporter) + +} // end namespace Catch diff --git a/test/object-store/util/event_loop.cpp b/test/object-store/util/event_loop.cpp index 98bc72e2858..f2143896bf3 100644 --- a/test/object-store/util/event_loop.cpp +++ b/test/object-store/util/event_loop.cpp @@ -31,6 +31,8 @@ #elif REALM_PLATFORM_APPLE #include #include +#elif REALM_ANDROID +// TODO: implement event loop for android: see Scheduler::make_alooper() #elif defined(__EMSCRIPTEN__) // TODO: implement event loop for Emscripten #else diff --git a/test/test_client_reset.cpp b/test/test_client_reset.cpp index 0d2ac3ef93b..0c2f1962d12 100644 --- a/test/test_client_reset.cpp +++ b/test/test_client_reset.cpp @@ -66,6 +66,7 @@ TEST(ClientReset_TransferGroupWithDanglingLinks) _impl::client_reset::transfer_group(*rt, *wt, *test_context.logger, allow_schema_additions); } +#if !REALM_MOBILE TEST(ClientReset_NoLocalChanges) { TEST_DIR(dir_1); // The original server dir. @@ -839,6 +840,7 @@ TEST(ClientReset_PinnedVersion) session.wait_for_download_complete_or_client_stopped(); } } +#endif // !REALM_MOBILE void mark_as_synchronized(DB& db) { diff --git a/test/test_crypto.cpp b/test/test_crypto.cpp index e59f1808988..de1c767785c 100644 --- a/test/test_crypto.cpp +++ b/test/test_crypto.cpp @@ -7,6 +7,9 @@ using namespace realm; using namespace realm::sync; +// testing sync server features on mobile is not supported +#if !REALM_MOBILE + static const char test_crypto_pubkey[] = "test_pubkey.pem"; static const StringData test_crypto_pubkey_data = "-----BEGIN PUBLIC KEY-----\n" @@ -73,6 +76,7 @@ TEST(Crypto_Verify_WithKeyFromBuffer) BinaryData sig{test_signature, sizeof test_signature - 1}; CHECK(key.verify(msg, sig)); } +#endif // REALM_MOBILE TEST(Crypto_SHA1) { diff --git a/test/test_sync.cpp b/test/test_sync.cpp index ccaec4b1a13..87d7e067761 100644 --- a/test/test_sync.cpp +++ b/test/test_sync.cpp @@ -126,6 +126,7 @@ ClientHistory& get_history(DBRef db) return get_replication(db).get_history(); } +#if !REALM_MOBILE // the server is not implemented on devices TEST(Sync_BadVirtualPath) { // NOTE: This test is no longer valid after migration to MongoDB Realm @@ -5588,50 +5589,6 @@ TEST(Sync_ServerSideEncryption) CHECK(group.has_table("class_Test")); } - -// This test calls row_for_object_id() for various object ids and tests that -// the right value is returned including that no assertions are hit. -TEST(Sync_RowForGlobalKey) -{ - TEST_CLIENT_DB(db); - - { - WriteTransaction wt(db); - TableRef table = wt.add_table("class_foo"); - table->add_column(type_Int, "i"); - wt.commit(); - } - - // Check that various object_ids are not in the table. - { - ReadTransaction rt(db); - ConstTableRef table = rt.get_table("class_foo"); - CHECK(table); - - // Default constructed GlobalKey - { - GlobalKey object_id; - auto row_ndx = table->get_objkey(object_id); - CHECK_NOT(row_ndx); - } - - // GlobalKey with small lo and hi values - { - GlobalKey object_id{12, 24}; - auto row_ndx = table->get_objkey(object_id); - CHECK_NOT(row_ndx); - } - - // GlobalKey with lo and hi values past the 32 bit limit. - { - GlobalKey object_id{uint_fast64_t(1) << 50, uint_fast64_t(1) << 52}; - auto row_ndx = table->get_objkey(object_id); - CHECK_NOT(row_ndx); - } - } -} - - TEST(Sync_LogCompaction_EraseObject_LinkList) { TEST_DIR(dir); @@ -6364,89 +6321,6 @@ TEST(Sync_Set) } } -TEST(Sync_DanglingLinksCountInPriorSize) -{ - SHARED_GROUP_TEST_PATH(path); - ClientReplication repl; - auto local_db = realm::DB::create(repl, path); - auto& history = repl.get_history(); - history.set_client_file_ident(sync::SaltedFileIdent{1, 123456}, true); - - version_type last_version, last_version_observed = 0; - auto dump_uploadable = [&] { - UploadCursor upload_cursor{last_version_observed, 0}; - std::vector changesets_to_upload; - version_type locked_server_version = 0; - history.find_uploadable_changesets(upload_cursor, last_version, changesets_to_upload, locked_server_version); - CHECK_EQUAL(changesets_to_upload.size(), static_cast(1)); - realm::sync::Changeset parsed_changeset; - auto unparsed_changeset = changesets_to_upload[0].changeset.get_first_chunk(); - realm::util::SimpleInputStream changeset_stream(unparsed_changeset); - realm::sync::parse_changeset(changeset_stream, parsed_changeset); - test_context.logger->info("changeset at version %1: %2", last_version, parsed_changeset); - last_version_observed = last_version; - return parsed_changeset; - }; - - TableKey source_table_key, target_table_key; - { - auto wt = local_db->start_write(); - auto source_table = wt->add_table_with_primary_key("class_source", type_String, "_id"); - auto target_table = wt->add_table_with_primary_key("class_target", type_String, "_id"); - source_table->add_column_list(*target_table, "links"); - - source_table_key = source_table->get_key(); - target_table_key = target_table->get_key(); - - auto obj_to_keep = target_table->create_object_with_primary_key(std::string{"target1"}); - auto obj_to_delete = target_table->create_object_with_primary_key(std::string{"target2"}); - auto source_obj = source_table->create_object_with_primary_key(std::string{"source"}); - - auto links_list = source_obj.get_linklist("links"); - links_list.add(obj_to_keep.get_key()); - links_list.add(obj_to_delete.get_key()); - last_version = wt->commit(); - } - - dump_uploadable(); - - { - // Simulate removing the object via the sync client so we get a dangling link - TempShortCircuitReplication disable_repl(repl); - auto wt = local_db->start_write(); - auto target_table = wt->get_table(target_table_key); - auto obj = target_table->get_object_with_primary_key(std::string{"target2"}); - obj.invalidate(); - last_version = wt->commit(); - } - - { - auto wt = local_db->start_write(); - auto source_table = wt->get_table(source_table_key); - auto target_table = wt->get_table(target_table_key); - - auto obj_to_add = target_table->create_object_with_primary_key(std::string{"target3"}); - - auto source_obj = source_table->get_object_with_primary_key(std::string{"source"}); - auto links_list = source_obj.get_linklist("links"); - links_list.add(obj_to_add.get_key()); - last_version = wt->commit(); - } - - auto changeset = dump_uploadable(); - CHECK_EQUAL(changeset.size(), static_cast(2)); - auto changeset_it = changeset.end(); - --changeset_it; - auto last_instr = *changeset_it; - CHECK_EQUAL(last_instr->type(), Instruction::Type::ArrayInsert); - auto arr_insert_instr = last_instr->get_as(); - CHECK_EQUAL(changeset.get_string(arr_insert_instr.table), StringData("source")); - CHECK(arr_insert_instr.value.type == sync::instr::Payload::Type::Link); - CHECK_EQUAL(changeset.get_string(mpark::get(arr_insert_instr.value.data.link.target)), - StringData("target3")); - CHECK_EQUAL(arr_insert_instr.prior_size, 2); -} - TEST(Sync_BundledRealmFile) { TEST_CLIENT_DB(db); @@ -6631,108 +6505,6 @@ TEST(Sync_MergeStringPrimaryKey) } } -TEST(Sync_NonIncreasingServerVersions) -{ - TEST_CLIENT_DB(db); - - auto& history = get_history(db); - history.set_client_file_ident(SaltedFileIdent{2, 0x1234567812345678}, false); - timestamp_type timestamp{1}; - history.set_local_origin_timestamp_source([&] { - return ++timestamp; - }); - - auto latest_local_version = [&] { - auto tr = db->start_write(); - tr->add_table_with_primary_key("class_foo", type_String, "_id")->add_column(type_Int, "int_col"); - return tr->commit(); - }(); - - std::vector server_changesets; - auto prep_changeset = [&](auto pk_name, auto int_col_val) { - Changeset changeset; - changeset.version = 10; - changeset.last_integrated_remote_version = latest_local_version - 1; - changeset.origin_timestamp = ++timestamp; - changeset.origin_file_ident = 1; - instr::PrimaryKey pk{changeset.intern_string(pk_name)}; - auto table_name = changeset.intern_string("foo"); - auto col_name = changeset.intern_string("int_col"); - instr::EraseObject erase_1; - erase_1.object = pk; - erase_1.table = table_name; - changeset.push_back(erase_1); - instr::CreateObject create_1; - create_1.object = pk; - create_1.table = table_name; - changeset.push_back(create_1); - instr::Update update_1; - update_1.table = table_name; - update_1.object = pk; - update_1.field = col_name; - update_1.value = instr::Payload{int64_t(int_col_val)}; - changeset.push_back(update_1); - server_changesets.push_back(std::move(changeset)); - }; - prep_changeset("bizz", 1); - prep_changeset("buzz", 2); - prep_changeset("baz", 3); - prep_changeset("bar", 4); - ++server_changesets.back().version; - - std::vector encoded; - std::vector server_changesets_encoded; - for (const auto& changeset : server_changesets) { - encoded.emplace_back(); - encode_changeset(changeset, encoded.back()); - server_changesets_encoded.emplace_back(changeset.version, changeset.last_integrated_remote_version, - BinaryData(encoded.back().data(), encoded.back().size()), - changeset.origin_timestamp, changeset.origin_file_ident); - } - - SyncProgress progress = {}; - progress.download.server_version = server_changesets.back().version; - progress.download.last_integrated_client_version = latest_local_version - 1; - progress.latest_server_version.version = server_changesets.back().version; - progress.latest_server_version.salt = 0x7876543217654321; - - uint_fast64_t downloadable_bytes = 0; - VersionInfo version_info; - auto transact = db->start_read(); - history.integrate_server_changesets(progress, &downloadable_bytes, server_changesets_encoded, version_info, - DownloadBatchState::SteadyState, *test_context.logger, transact); -} - -TEST(Sync_InvalidChangesetFromServer) -{ - TEST_CLIENT_DB(db); - - auto& history = get_history(db); - history.set_client_file_ident(SaltedFileIdent{2, 0x1234567812345678}, false); - - instr::CreateObject bad_instr; - bad_instr.object = InternString{1}; - bad_instr.table = InternString{2}; - - Changeset changeset; - changeset.push_back(bad_instr); - - ChangesetEncoder::Buffer encoded; - encode_changeset(changeset, encoded); - RemoteChangeset server_changeset; - server_changeset.origin_file_ident = 1; - server_changeset.remote_version = 1; - server_changeset.data = BinaryData(encoded.data(), encoded.size()); - - VersionInfo version_info; - auto transact = db->start_read(); - CHECK_THROW_EX(history.integrate_server_changesets({}, nullptr, util::Span(&server_changeset, 1), version_info, - DownloadBatchState::SteadyState, *test_context.logger, - transact), - sync::IntegrationException, - StringData(e.what()).contains("Failed to parse received changeset: Invalid interned string")); -} - TEST(Sync_DifferentUsersMultiplexing) { ClientServerFixture::Config fixture_config; @@ -6776,30 +6548,116 @@ TEST(Sync_DifferentUsersMultiplexing) user_2_sess_2.sess.get_appservices_connection_id()); } -// Tests that an empty reciprocal changesets is set and retrieved correctly. -TEST(Sync_SetAndGetEmptyReciprocalChangeset) +TEST(Sync_TransformAgainstEmptyReciprocalChangeset) { - using namespace realm; - using namespace realm::sync::instr; - using realm::sync::Changeset; - - TEST_CLIENT_DB(db); - - auto& history = get_history(db); - history.set_client_file_ident(SaltedFileIdent{1, 0x1234567812345678}, false); - timestamp_type timestamp{1}; - history.set_local_origin_timestamp_source([&] { - return ++timestamp; - }); + TEST_CLIENT_DB(seed_db); + TEST_CLIENT_DB(db_1); + TEST_CLIENT_DB(db_2); - auto latest_local_version = [&] { - auto tr = db->start_write(); + { + auto tr = seed_db->start_write(); // Create schema: single table with array of ints as property. - tr->add_table_with_primary_key("class_table", type_Int, "_id")->add_column_list(type_Int, "ints"); + auto table = tr->add_table_with_primary_key("class_table", type_Int, "_id"); + table->add_column_list(type_Int, "ints"); + table->add_column(type_String, "string"); tr->commit_and_continue_writing(); // Create object and initialize array. - TableRef table = tr->get_table("class_table"); + table = tr->get_table("class_table"); + auto obj = table->create_object_with_primary_key(42); + auto ints = obj.get_list("ints"); + for (auto i = 0; i < 8; ++i) { + ints.insert(i, i); + } + tr->commit(); + } + + { + TEST_DIR(dir); + MultiClientServerFixture fixture(3, 1, dir, test_context); + fixture.start(); + + util::Optional seed_session = fixture.make_bound_session(0, seed_db, 0, "/test"); + util::Optional db_1_session = fixture.make_bound_session(1, db_1, 0, "/test"); + util::Optional db_2_session = fixture.make_bound_session(2, db_2, 0, "/test"); + + seed_session->wait_for_upload_complete_or_client_stopped(); + db_1_session->wait_for_download_complete_or_client_stopped(); + db_2_session->wait_for_download_complete_or_client_stopped(); + seed_session.reset(); + db_2_session.reset(); + + auto move_element = [&](const DBRef& db, size_t from, size_t to, size_t string_size = 0) { + auto wt = db->start_write(); + auto table = wt->get_table("class_table"); + auto obj = table->get_object_with_primary_key(42); + auto ints = obj.get_list("ints"); + ints.move(from, to); + obj.set("string", std::string(string_size, 'a')); + wt->commit(); + }; + + // Client 1 uploads two move instructions. + move_element(db_1, 7, 2); + move_element(db_1, 7, 6); + + db_1_session->wait_for_upload_complete_or_client_stopped(); + + std::this_thread::sleep_for(std::chrono::milliseconds{10}); + + // Client 2 uploads two move instructions. + // The sync client uploads at most 128 KB of data so we make the first changeset large enough so two upload + // messages are sent to the server instead of one. Each change is transformed against the changes from + // Client 1. + + // First change discards the first change (move(7, 2)) of Client 1. + move_element(db_2, 7, 0, 200 * 1024); + // Second change is tranformed against an empty reciprocal changeset as result of the change above. + move_element(db_2, 7, 5); + db_2_session = fixture.make_bound_session(2, db_2, 0, "/test"); + + db_1_session->wait_for_upload_complete_or_client_stopped(); + db_2_session->wait_for_upload_complete_or_client_stopped(); + + db_1_session->wait_for_download_complete_or_client_stopped(); + db_2_session->wait_for_download_complete_or_client_stopped(); + } + + ReadTransaction rt_1(db_1); + ReadTransaction rt_2(db_2); + const Group& group_1 = rt_1; + const Group& group_2 = rt_2; + group_1.verify(); + group_2.verify(); + CHECK(compare_groups(rt_1, rt_2)); +} + +#endif // !REALM_MOBILE + +// Tests that an empty reciprocal changesets is set and retrieved correctly. +TEST(Sync_SetAndGetEmptyReciprocalChangeset) +{ + using namespace realm; + using namespace realm::sync::instr; + using realm::sync::Changeset; + + TEST_CLIENT_DB(db); + + auto& history = get_history(db); + history.set_client_file_ident(SaltedFileIdent{1, 0x1234567812345678}, false); + timestamp_type timestamp{1}; + history.set_local_origin_timestamp_source([&] { + return ++timestamp; + }); + + auto latest_local_version = [&] { + auto tr = db->start_write(); + // Create schema: single table with array of ints as property. + tr->add_table_with_primary_key("class_table", type_Int, "_id")->add_column_list(type_Int, "ints"); + tr->commit_and_continue_writing(); + + // Create object and initialize array. + TableRef table = tr->get_table("class_table"); auto obj = table->create_object_with_primary_key(42); auto ints = obj.get_list("ints"); for (auto i = 0; i < 8; ++i) { @@ -6865,88 +6723,34 @@ TEST(Sync_SetAndGetEmptyReciprocalChangeset) CHECK(reciprocal_changeset.empty()); } -TEST(Sync_TransformAgainstEmptyReciprocalChangeset) +TEST(Sync_InvalidChangesetFromServer) { - TEST_CLIENT_DB(seed_db); - TEST_CLIENT_DB(db_1); - TEST_CLIENT_DB(db_2); - - { - auto tr = seed_db->start_write(); - // Create schema: single table with array of ints as property. - auto table = tr->add_table_with_primary_key("class_table", type_Int, "_id"); - table->add_column_list(type_Int, "ints"); - table->add_column(type_String, "string"); - tr->commit_and_continue_writing(); - - // Create object and initialize array. - table = tr->get_table("class_table"); - auto obj = table->create_object_with_primary_key(42); - auto ints = obj.get_list("ints"); - for (auto i = 0; i < 8; ++i) { - ints.insert(i, i); - } - tr->commit(); - } - - { - TEST_DIR(dir); - MultiClientServerFixture fixture(3, 1, dir, test_context); - fixture.start(); - - util::Optional seed_session = fixture.make_bound_session(0, seed_db, 0, "/test"); - util::Optional db_1_session = fixture.make_bound_session(1, db_1, 0, "/test"); - util::Optional db_2_session = fixture.make_bound_session(2, db_2, 0, "/test"); - - seed_session->wait_for_upload_complete_or_client_stopped(); - db_1_session->wait_for_download_complete_or_client_stopped(); - db_2_session->wait_for_download_complete_or_client_stopped(); - seed_session.reset(); - db_2_session.reset(); - - auto move_element = [&](const DBRef& db, size_t from, size_t to, size_t string_size = 0) { - auto wt = db->start_write(); - auto table = wt->get_table("class_table"); - auto obj = table->get_object_with_primary_key(42); - auto ints = obj.get_list("ints"); - ints.move(from, to); - obj.set("string", std::string(string_size, 'a')); - wt->commit(); - }; - - // Client 1 uploads two move instructions. - move_element(db_1, 7, 2); - move_element(db_1, 7, 6); - - db_1_session->wait_for_upload_complete_or_client_stopped(); - - std::this_thread::sleep_for(std::chrono::milliseconds{10}); + TEST_CLIENT_DB(db); - // Client 2 uploads two move instructions. - // The sync client uploads at most 128 KB of data so we make the first changeset large enough so two upload - // messages are sent to the server instead of one. Each change is transformed against the changes from - // Client 1. + auto& history = get_history(db); + history.set_client_file_ident(SaltedFileIdent{2, 0x1234567812345678}, false); - // First change discards the first change (move(7, 2)) of Client 1. - move_element(db_2, 7, 0, 200 * 1024); - // Second change is tranformed against an empty reciprocal changeset as result of the change above. - move_element(db_2, 7, 5); - db_2_session = fixture.make_bound_session(2, db_2, 0, "/test"); + instr::CreateObject bad_instr; + bad_instr.object = InternString{1}; + bad_instr.table = InternString{2}; - db_1_session->wait_for_upload_complete_or_client_stopped(); - db_2_session->wait_for_upload_complete_or_client_stopped(); + Changeset changeset; + changeset.push_back(bad_instr); - db_1_session->wait_for_download_complete_or_client_stopped(); - db_2_session->wait_for_download_complete_or_client_stopped(); - } + ChangesetEncoder::Buffer encoded; + encode_changeset(changeset, encoded); + RemoteChangeset server_changeset; + server_changeset.origin_file_ident = 1; + server_changeset.remote_version = 1; + server_changeset.data = BinaryData(encoded.data(), encoded.size()); - ReadTransaction rt_1(db_1); - ReadTransaction rt_2(db_2); - const Group& group_1 = rt_1; - const Group& group_2 = rt_2; - group_1.verify(); - group_2.verify(); - CHECK(compare_groups(rt_1, rt_2, *test_context.logger)); + VersionInfo version_info; + auto transact = db->start_read(); + CHECK_THROW_EX(history.integrate_server_changesets({}, nullptr, util::Span(&server_changeset, 1), version_info, + DownloadBatchState::SteadyState, *test_context.logger, + transact), + sync::IntegrationException, + StringData(e.what()).contains("Failed to parse received changeset: Invalid interned string")); } TEST(Sync_ServerVersionsSkippedFromDownloadCursor) @@ -7009,4 +6813,202 @@ TEST(Sync_ServerVersionsSkippedFromDownloadCursor) expected_progress.upload.last_integrated_server_version); } +TEST(Sync_NonIncreasingServerVersions) +{ + TEST_CLIENT_DB(db); + + auto& history = get_history(db); + history.set_client_file_ident(SaltedFileIdent{2, 0x1234567812345678}, false); + timestamp_type timestamp{1}; + history.set_local_origin_timestamp_source([&] { + return ++timestamp; + }); + + auto latest_local_version = [&] { + auto tr = db->start_write(); + tr->add_table_with_primary_key("class_foo", type_String, "_id")->add_column(type_Int, "int_col"); + return tr->commit(); + }(); + + std::vector server_changesets; + auto prep_changeset = [&](auto pk_name, auto int_col_val) { + Changeset changeset; + changeset.version = 10; + changeset.last_integrated_remote_version = latest_local_version - 1; + changeset.origin_timestamp = ++timestamp; + changeset.origin_file_ident = 1; + instr::PrimaryKey pk{changeset.intern_string(pk_name)}; + auto table_name = changeset.intern_string("foo"); + auto col_name = changeset.intern_string("int_col"); + instr::EraseObject erase_1; + erase_1.object = pk; + erase_1.table = table_name; + changeset.push_back(erase_1); + instr::CreateObject create_1; + create_1.object = pk; + create_1.table = table_name; + changeset.push_back(create_1); + instr::Update update_1; + update_1.table = table_name; + update_1.object = pk; + update_1.field = col_name; + update_1.value = instr::Payload{int64_t(int_col_val)}; + changeset.push_back(update_1); + server_changesets.push_back(std::move(changeset)); + }; + prep_changeset("bizz", 1); + prep_changeset("buzz", 2); + prep_changeset("baz", 3); + prep_changeset("bar", 4); + ++server_changesets.back().version; + + std::vector encoded; + std::vector server_changesets_encoded; + for (const auto& changeset : server_changesets) { + encoded.emplace_back(); + encode_changeset(changeset, encoded.back()); + server_changesets_encoded.emplace_back(changeset.version, changeset.last_integrated_remote_version, + BinaryData(encoded.back().data(), encoded.back().size()), + changeset.origin_timestamp, changeset.origin_file_ident); + } + + SyncProgress progress = {}; + progress.download.server_version = server_changesets.back().version; + progress.download.last_integrated_client_version = latest_local_version - 1; + progress.latest_server_version.version = server_changesets.back().version; + progress.latest_server_version.salt = 0x7876543217654321; + + uint_fast64_t downloadable_bytes = 0; + VersionInfo version_info; + auto transact = db->start_read(); + history.integrate_server_changesets(progress, &downloadable_bytes, server_changesets_encoded, version_info, + DownloadBatchState::SteadyState, *test_context.logger, transact); +} + +TEST(Sync_DanglingLinksCountInPriorSize) +{ + SHARED_GROUP_TEST_PATH(path); + ClientReplication repl; + auto local_db = realm::DB::create(repl, path); + auto& history = repl.get_history(); + history.set_client_file_ident(sync::SaltedFileIdent{1, 123456}, true); + + version_type last_version, last_version_observed = 0; + auto dump_uploadable = [&] { + UploadCursor upload_cursor{last_version_observed, 0}; + std::vector changesets_to_upload; + version_type locked_server_version = 0; + history.find_uploadable_changesets(upload_cursor, last_version, changesets_to_upload, locked_server_version); + CHECK_EQUAL(changesets_to_upload.size(), static_cast(1)); + realm::sync::Changeset parsed_changeset; + auto unparsed_changeset = changesets_to_upload[0].changeset.get_first_chunk(); + realm::util::SimpleInputStream changeset_stream(unparsed_changeset); + realm::sync::parse_changeset(changeset_stream, parsed_changeset); + test_context.logger->info("changeset at version %1: %2", last_version, parsed_changeset); + last_version_observed = last_version; + return parsed_changeset; + }; + + TableKey source_table_key, target_table_key; + { + auto wt = local_db->start_write(); + auto source_table = wt->add_table_with_primary_key("class_source", type_String, "_id"); + auto target_table = wt->add_table_with_primary_key("class_target", type_String, "_id"); + source_table->add_column_list(*target_table, "links"); + + source_table_key = source_table->get_key(); + target_table_key = target_table->get_key(); + + auto obj_to_keep = target_table->create_object_with_primary_key(std::string{"target1"}); + auto obj_to_delete = target_table->create_object_with_primary_key(std::string{"target2"}); + auto source_obj = source_table->create_object_with_primary_key(std::string{"source"}); + + auto links_list = source_obj.get_linklist("links"); + links_list.add(obj_to_keep.get_key()); + links_list.add(obj_to_delete.get_key()); + last_version = wt->commit(); + } + + dump_uploadable(); + + { + // Simulate removing the object via the sync client so we get a dangling link + TempShortCircuitReplication disable_repl(repl); + auto wt = local_db->start_write(); + auto target_table = wt->get_table(target_table_key); + auto obj = target_table->get_object_with_primary_key(std::string{"target2"}); + obj.invalidate(); + last_version = wt->commit(); + } + + { + auto wt = local_db->start_write(); + auto source_table = wt->get_table(source_table_key); + auto target_table = wt->get_table(target_table_key); + + auto obj_to_add = target_table->create_object_with_primary_key(std::string{"target3"}); + + auto source_obj = source_table->get_object_with_primary_key(std::string{"source"}); + auto links_list = source_obj.get_linklist("links"); + links_list.add(obj_to_add.get_key()); + last_version = wt->commit(); + } + + auto changeset = dump_uploadable(); + CHECK_EQUAL(changeset.size(), static_cast(2)); + auto changeset_it = changeset.end(); + --changeset_it; + auto last_instr = *changeset_it; + CHECK_EQUAL(last_instr->type(), Instruction::Type::ArrayInsert); + auto arr_insert_instr = last_instr->get_as(); + CHECK_EQUAL(changeset.get_string(arr_insert_instr.table), StringData("source")); + CHECK(arr_insert_instr.value.type == sync::instr::Payload::Type::Link); + CHECK_EQUAL(changeset.get_string(mpark::get(arr_insert_instr.value.data.link.target)), + StringData("target3")); + CHECK_EQUAL(arr_insert_instr.prior_size, 2); +} + +// This test calls row_for_object_id() for various object ids and tests that +// the right value is returned including that no assertions are hit. +TEST(Sync_RowForGlobalKey) +{ + TEST_CLIENT_DB(db); + + { + WriteTransaction wt(db); + TableRef table = wt.add_table("class_foo"); + table->add_column(type_Int, "i"); + wt.commit(); + } + + // Check that various object_ids are not in the table. + { + ReadTransaction rt(db); + ConstTableRef table = rt.get_table("class_foo"); + CHECK(table); + + // Default constructed GlobalKey + { + GlobalKey object_id; + auto row_ndx = table->get_objkey(object_id); + CHECK_NOT(row_ndx); + } + + // GlobalKey with small lo and hi values + { + GlobalKey object_id{12, 24}; + auto row_ndx = table->get_objkey(object_id); + CHECK_NOT(row_ndx); + } + + // GlobalKey with lo and hi values past the 32 bit limit. + { + GlobalKey object_id{uint_fast64_t(1) << 50, uint_fast64_t(1) << 52}; + auto row_ndx = table->get_objkey(object_id); + CHECK_NOT(row_ndx); + } + } +} + + } // unnamed namespace diff --git a/test/test_sync_auth.cpp b/test/test_sync_auth.cpp index d13538588db..9969e266a5e 100644 --- a/test/test_sync_auth.cpp +++ b/test/test_sync_auth.cpp @@ -16,6 +16,8 @@ using namespace realm::sync; namespace { +#if !REALM_MOBILE + TEST(Sync_Auth_JWTAccessToken) { AccessToken tok; @@ -79,4 +81,6 @@ TEST(Sync_Auth_JWTAccessTokenStitchFields) CHECK_EQUAL(tok.access, admin_access); } +#endif // !REALM_MOBILE + } // unnamed namespace diff --git a/test/test_sync_history_migration.cpp b/test/test_sync_history_migration.cpp index 054f7ba1015..7be5d899379 100644 --- a/test/test_sync_history_migration.cpp +++ b/test/test_sync_history_migration.cpp @@ -47,6 +47,7 @@ using namespace realm::test_util; namespace { +#if !REALM_MOBILE TEST(Sync_HistoryMigration) { // Set to true to produce new versions of client and server-side files in @@ -439,6 +440,8 @@ TEST(Sync_HistoryMigration) CHECK_NOT(produce_new_files); // Should not be enabled under normal circumstances } +#endif // !REALM_MOBILE + TEST(Sync_HistoryCompression) { SHARED_GROUP_TEST_PATH(path); diff --git a/test/test_util_network_ssl.cpp b/test/test_util_network_ssl.cpp index d36ff353b10..9d260527333 100644 --- a/test/test_util_network_ssl.cpp +++ b/test/test_util_network_ssl.cpp @@ -40,7 +40,7 @@ using namespace realm::util; // `experiments/testcase.cpp` and then run `sh build.sh // check-testcase` (or one of its friends) from the command line. - +#if !REALM_MOBILE namespace { network::Endpoint bind_acceptor(network::Acceptor& acceptor) @@ -1215,3 +1215,5 @@ TEST(Util_Network_SSL_Certificate_Failure) thread_1.join(); thread_2.join(); } + +#endif // !REALM_MOBILE diff --git a/test/util/compare_groups.cpp b/test/util/compare_groups.cpp index 1f68ba1fb1b..60c87a0ece6 100644 --- a/test/util/compare_groups.cpp +++ b/test/util/compare_groups.cpp @@ -48,9 +48,9 @@ class TableCompareLogger : public util::Logger { class ObjectCompareLogger : public util::Logger { public: - ObjectCompareLogger(sync::PrimaryKey oid, util::Logger& base_logger) noexcept + ObjectCompareLogger(Mixed pk, util::Logger& base_logger) noexcept : util::Logger(base_logger.get_level_threshold()) - , m_oid{oid} + , m_pk{pk} , m_base_logger{base_logger} { } @@ -61,7 +61,7 @@ class ObjectCompareLogger : public util::Logger { } private: - const sync::PrimaryKey m_oid; + const Mixed m_pk; util::Logger& m_base_logger; std::string m_prefix; void ensure_prefix() @@ -69,7 +69,7 @@ class ObjectCompareLogger : public util::Logger { if (REALM_LIKELY(!m_prefix.empty())) return; std::ostringstream out; - out << sync::format_pk(m_oid) << ": "; // Throws + out << m_pk << ": "; // Throws m_prefix = out.str(); // Throws } }; @@ -167,100 +167,15 @@ struct Column { } }; -sync::PrimaryKey primary_key_for_row(const Obj& obj) -{ - auto table = obj.get_table(); - ColKey pk_col = table->get_primary_key_column(); - if (pk_col) { - ColumnType pk_type = pk_col.get_type(); - if (obj.is_null(pk_col)) { - return mpark::monostate{}; - } - - if (pk_type == col_type_Int) { - return obj.get(pk_col); - } - - if (pk_type == col_type_String) { - return obj.get(pk_col); - } - - if (pk_type == col_type_ObjectId) { - return obj.get(pk_col); - } - - if (pk_type == col_type_UUID) { - return obj.get(pk_col); - } - - REALM_TERMINATE("Missing primary key type support"); - } - - GlobalKey global_key = obj.get_object_id(); - return global_key; -} - -sync::PrimaryKey primary_key_for_row(const Table& table, ObjKey key) -{ - auto obj = table.get_object(key); - return primary_key_for_row(obj); -} - -ObjKey row_for_primary_key(const Table& table, sync::PrimaryKey key) +ObjKey row_for_primary_key(const Table& table, Mixed pk) { ColKey pk_col = table.get_primary_key_column(); if (pk_col) { - ColumnType pk_type = pk_col.get_type(); - - if (auto pk = mpark::get_if(&key)) { - static_cast(pk); - if (!pk_col.is_nullable()) { - REALM_TERMINATE("row_for_primary_key with null on non-nullable primary key column"); - } - return table.find_primary_key({}); - } - - if (pk_type == col_type_Int) { - if (auto pk = mpark::get_if(&key)) { - return table.find_primary_key(*pk); - } - else { - REALM_TERMINATE("row_for_primary_key mismatching primary key type (expected int)"); - } - } - - if (pk_type == col_type_String) { - if (auto pk = mpark::get_if(&key)) { - return table.find_primary_key(*pk); - } - else { - REALM_TERMINATE("row_for_primary_key mismatching primary key type (expected string)"); - } - } - - if (pk_type == col_type_ObjectId) { - if (auto pk = mpark::get_if(&key)) { - return table.find_primary_key(*pk); - } - else { - REALM_TERMINATE("row_for_primary_key mismatching primary key type (expected ObjectId)"); - } - } - - if (pk_type == col_type_UUID) { - if (auto pk = mpark::get_if(&key)) { - return table.find_primary_key(*pk); - } - else { - REALM_TERMINATE("row_for_primary_key mismatching primary key type (expected UUID)"); - } - } - - REALM_TERMINATE("row_for_primary_key missing primary key type support"); + return table.find_primary_key(pk); // will assert on type mismatch } - if (auto global_key = mpark::get_if(&key)) { - return table.get_objkey(*global_key); + if (pk.is_type(type_Link, type_TypedLink)) { + return pk.get(); } else { REALM_TERMINATE("row_for_primary_key() with primary key, expected GlobalKey"); @@ -269,8 +184,8 @@ ObjKey row_for_primary_key(const Table& table, sync::PrimaryKey key) } bool compare_objects(const Obj& obj_1, const Obj& obj_2, const std::vector& columns, util::Logger& logger); -bool compare_objects(sync::PrimaryKey& oid, const Table& table_1, const Table& table_2, - const std::vector& columns, util::Logger& logger); +bool compare_objects(Mixed& oid, const Table& table_1, const Table& table_2, const std::vector& columns, + util::Logger& logger); bool compare_schemas(const Table& table_1, const Table& table_2, util::Logger& logger, std::vector* out_columns = nullptr) @@ -526,8 +441,8 @@ bool compare_lists(const Column& col, const Obj& obj_1, const Obj& obj_2, util:: } } else { - sync::PrimaryKey target_oid_1 = primary_key_for_row(*target_table_1, link_1); - sync::PrimaryKey target_oid_2 = primary_key_for_row(*target_table_2, link_2); + Mixed target_oid_1 = target_table_1->get_primary_key(link_1); + Mixed target_oid_2 = target_table_2->get_primary_key(link_2); if (target_oid_1 != target_oid_2) { logger.error("Value mismatch in column '%1' at index %2 of the link " "list (%3 vs %4)", @@ -685,8 +600,8 @@ bool compare_sets(const Column& col, const Obj& obj_1, const Obj& obj_2, util::L } } else { - sync::PrimaryKey target_oid_1 = primary_key_for_row(*target_table_1, link_1); - sync::PrimaryKey target_oid_2 = primary_key_for_row(*target_table_2, link_2); + Mixed target_oid_1 = target_table_1->get_primary_key(link_1); + Mixed target_oid_2 = target_table_2->get_primary_key(link_2); if (target_oid_1 != target_oid_2) { logger.error("Value mismatch in column '%1' at index %2 of the link " "set (%3 vs %4)", @@ -919,11 +834,11 @@ bool compare_objects(const Obj& obj_1, const Obj& obj_2, const std::vectorget_primary_key(link_1); + Mixed target_oid_2 = target_table_2->get_primary_key(link_2); if (target_oid_1 != target_oid_2) { - logger.error("Value mismatch in column '%1' (%2 vs %3)", col.name, - sync::format_pk(target_oid_1), sync::format_pk(target_oid_2)); + logger.error("Value mismatch in column '%1' (%2 vs %3)", col.name, target_oid_1, + target_oid_2); equal = false; } } @@ -939,17 +854,17 @@ bool compare_objects(const Obj& obj_1, const Obj& obj_2, const std::vector& columns, util::Logger& logger) +bool compare_objects(Mixed& pk, const Table& table_1, const Table& table_2, const std::vector& columns, + util::Logger& logger) { - ObjKey row_1 = row_for_primary_key(table_1, oid); - ObjKey row_2 = row_for_primary_key(table_2, oid); + ObjKey oid_1 = row_for_primary_key(table_1, pk); + ObjKey oid_2 = row_for_primary_key(table_2, pk); // Note: This is ensured by the inventory handling in compare_tables(). - REALM_ASSERT(row_1); - REALM_ASSERT(row_2); - const Obj obj_1 = table_1.get_object(row_1); - const Obj obj_2 = table_2.get_object(row_2); + REALM_ASSERT(oid_1); + REALM_ASSERT(oid_2); + const Obj obj_1 = table_1.get_object(oid_1); + const Obj obj_2 = table_2.get_object(oid_2); return compare_objects(obj_1, obj_2, columns, logger); } @@ -986,18 +901,17 @@ bool compare_tables(const Table& table_1, const Table& table_2, util::Logger& lo } // Compare row sets - using Objects = std::set; + using Objects = std::set; auto make_inventory = [](const Table& table, Objects& objects) { for (const Obj& obj : table) { - auto oid = primary_key_for_row(obj); - objects.insert(oid); + objects.insert(obj.get_primary_key()); } }; Objects objects_1, objects_2; make_inventory(table_1, objects_1); make_inventory(table_2, objects_2); auto report_missing = [&](const char* hand_2, Objects& objects_1, Objects& objects_2) { - std::vector missing; + std::vector missing; for (auto oid : objects_1) { if (objects_2.find(oid) == objects_2.end()) missing.push_back(oid); @@ -1006,15 +920,15 @@ bool compare_tables(const Table& table_1, const Table& table_2, util::Logger& lo return; std::size_t n = missing.size(); if (n == 1) { - logger.error("One object missing in %1 side table: %2", hand_2, sync::format_pk(missing[0])); + logger.error("One object missing in %1 side table: %2", hand_2, missing[0]); equal = false; return; } std::ostringstream out; - out << sync::format_pk(missing[0]); + out << missing[0]; std::size_t m = std::min(4, n); for (std::size_t i = 1; i < m; ++i) - out << ", " << sync::format_pk(missing[i]); + out << ", " << missing[i]; if (m < n) out << ", ..."; logger.error("%1 objects missing in %2 side table: %3", n, hand_2, out.str()); @@ -1024,10 +938,10 @@ bool compare_tables(const Table& table_1, const Table& table_2, util::Logger& lo report_missing("left-hand", objects_2, objects_1); // Compare individual rows - for (auto oid : objects_1) { - if (objects_2.find(oid) != objects_2.end()) { - ObjectCompareLogger sublogger{oid, logger}; - if (!compare_objects(oid, table_1, table_2, columns, sublogger)) { + for (auto pk : objects_1) { + if (objects_2.find(pk) != objects_2.end()) { + ObjectCompareLogger sublogger{pk, logger}; + if (!compare_objects(pk, table_1, table_2, columns, sublogger)) { equal = false; } }