LCOV - code coverage report
Current view: top level - /opt/artdaq/srcs/artdaq-database/test/CaseStudy/OtsDaqTests/test002 - DuplicateDetection_t.cc (source / functions) Coverage Total Hit
Test: artdaq.info.cleaned Lines: 91.2 % 501 457
Test Date: 2026-10-29 00:35:58 Functions: 92.3 % 39 36

            Line data    Source code
       1              : #define BOOST_TEST_DYN_LINK
       2              : #define BOOST_TEST_MODULE (DuplicateDetection test)
       3              : 
       4              : #include <boost/test/unit_test.hpp>
       5              : 
       6              : #include <dirent.h>
       7              : #include <cassert>
       8              : #include <chrono>
       9              : #include <cstdlib>
      10              : #include <ctime>
      11              : #include <fstream>
      12              : #include <iostream>
      13              : #include <map>
      14              : #include <memory>
      15              : #include <regex>
      16              : #include <set>
      17              : #include <sstream>
      18              : #include <string>
      19              : #include <vector>
      20              : 
      21              : #include "../DatabaseConfigurationInterface.h"
      22              : #include "artdaq-database/JsonDocument/JSONDocument.h"
      23              : 
      24              : #include "artdaq-database/ConfigurationDB/Multitasker.h"
      25              : #include "artdaq-database/ConfigurationDB/configuration_common.h"
      26              : #include "artdaq-database/ConfigurationDB/dispatch_common.h"
      27              : 
      28            4 : bool jsonContains(const std::string& json, const std::string& key, const std::string& value) {
      29            4 :   std::string pattern1 = "\"" + key + "\":\"" + value + "\"";
      30            4 :   std::string pattern2 = "\"" + key + "\" : \"" + value + "\"";
      31            8 :   return json.find(pattern1) != std::string::npos || json.find(pattern2) != std::string::npos;
      32            4 : }
      33              : 
      34           39 : std::string readFileContent(const std::string& filepath) {
      35           39 :   std::ifstream file(filepath);
      36           39 :   if (!file.is_open()) {
      37            0 :     return "";
      38              :   }
      39           39 :   std::stringstream buffer;
      40           39 :   buffer << file.rdbuf();
      41           39 :   return buffer.str();
      42           39 : }
      43              : 
      44            5 : std::vector<std::string> listJsonFiles(const std::string& dirPath) {
      45            5 :   std::vector<std::string> files;
      46              : 
      47            5 :   DIR* dir = opendir(dirPath.c_str());
      48            5 :   if (dir == nullptr) {
      49            0 :     return files;
      50              :   }
      51              : 
      52              :   struct dirent* entry;
      53           73 :   while ((entry = readdir(dir)) != nullptr) {
      54           68 :     std::string name = entry->d_name;
      55           68 :     if (name.size() > 5 && name.substr(name.size() - 5) == ".json" && name != "index.json") {
      56           53 :       files.push_back(dirPath + "/" + name);
      57              :     }
      58           68 :   }
      59            5 :   closedir(dir);
      60            5 :   return files;
      61            0 : }
      62              : 
      63            2 : int countDocumentsWithVersion(const std::string& dirPath, const std::string& configType, const std::string& version) {
      64            2 :   auto files = listJsonFiles(dirPath);
      65            2 :   int count = 0;
      66              : 
      67            2 :   std::regex versionPattern("\"version\"\\s*:\\s*\"" + version + "\"");
      68              : 
      69           14 :   for (const auto& filepath : files) {
      70           12 :     std::string content = readFileContent(filepath);
      71           12 :     if (content.empty()) continue;
      72              : 
      73           12 :     if (content.find("\"" + configType + "\"") != std::string::npos && std::regex_search(content, versionPattern)) {
      74            2 :       count++;
      75              :     }
      76           12 :   }
      77            2 :   return count;
      78            2 : }
      79              : 
      80            6 : bool isFileSystemBackend() {
      81            6 :   auto uri = std::getenv("ARTDAQ_DATABASE_URI");
      82            6 :   if (uri) {
      83            6 :     std::string uriStr(uri);
      84            6 :     return uriStr.find("filesystemdb") != std::string::npos;
      85            6 :   }
      86            0 :   return false;
      87              : }
      88              : 
      89            2 : std::string getDatabasePath() {
      90            2 :   auto uri = std::getenv("ARTDAQ_DATABASE_URI");
      91            2 :   if (uri) {
      92            2 :     std::string uriStr(uri);
      93            2 :     if (uriStr.find("filesystemdb:///") == 0) {
      94            1 :       return uriStr.substr(16);
      95            1 :     } else if (uriStr.find("filesystemdb://") == 0) {
      96            0 :       return uriStr.substr(15);
      97              :     }
      98            1 :     return uriStr;
      99            2 :   }
     100            0 :   return "";
     101              : }
     102              : 
     103            0 : std::set<std::string> extractOidsFromIndex(const std::string& indexPath) {
     104            0 :   std::set<std::string> oids;
     105            0 :   std::string content = readFileContent(indexPath);
     106            0 :   if (content.empty()) {
     107            0 :     return oids;
     108              :   }
     109              : 
     110            0 :   std::regex pattern("\"oid\"\\s*:\\s*\"([a-f0-9]+)\"");
     111            0 :   auto begin = std::sregex_iterator(content.begin(), content.end(), pattern);
     112            0 :   auto end = std::sregex_iterator();
     113              : 
     114            0 :   for (auto it = begin; it != end; ++it) {
     115            0 :     oids.insert((*it)[1].str());
     116            0 :   }
     117            0 :   return oids;
     118            0 : }
     119              : 
     120            0 : std::string getOidFromFilename(const std::string& filepath) {
     121            0 :   size_t lastSlash = filepath.rfind('/');
     122            0 :   std::string filename = (lastSlash != std::string::npos) ? filepath.substr(lastSlash + 1) : filepath;
     123              : 
     124            0 :   if (filename.size() > 5 && filename.substr(filename.size() - 5) == ".json") {
     125            0 :     return filename.substr(0, filename.size() - 5);
     126              :   }
     127            0 :   return "";
     128            0 : }
     129              : 
     130            0 : int countIndexEntriesForVersion(const std::string& indexPath, const std::string& version) {
     131            0 :   std::string content = readFileContent(indexPath);
     132            0 :   if (content.empty()) return 0;
     133              : 
     134            0 :   std::regex pattern("\"version\"\\s*:\\s*\"" + version + "\"");
     135            0 :   auto begin = std::sregex_iterator(content.begin(), content.end(), pattern);
     136            0 :   auto end = std::sregex_iterator();
     137              : 
     138            0 :   return std::distance(begin, end);
     139            0 : }
     140              : 
     141              : struct DuplicateTestData {
     142            2 :   DuplicateTestData() {
     143            2 :     srand(time(nullptr));
     144            2 :     baseVersion = rand() % 99999 + 100000;
     145            2 :     dbPath = getDatabasePath();
     146            2 :     std::cout << "=== DUPLICATE DETECTION TEST SUITE ===\n";
     147            2 :     std::cout << "Base version: " << baseVersion << "\n";
     148            2 :     std::cout << "Database path: " << dbPath << "\n\n";
     149            2 :   }
     150              : 
     151            2 :   ~DuplicateTestData() { std::cout << "\n=== TEST SUITE COMPLETE ===\n"; }
     152              : 
     153              :   void updateConfigCount(int count) { oldConfigCount = count; }
     154              : 
     155              :   int baseVersion;
     156              :   int oldConfigCount = 0;
     157              :   std::string dbPath;
     158              : };
     159              : 
     160              : using namespace ots;
     161              : 
     162              : DuplicateTestData fixture;
     163              : 
     164              : BOOST_AUTO_TEST_SUITE(duplicate_detection_test)
     165              : 
     166            4 : BOOST_AUTO_TEST_CASE(duplicate_version_rejected) {
     167            2 :   std::cout << "\n=== TEST 1: Duplicate version rejected ===\n";
     168              : 
     169            2 :   auto ifc = DatabaseConfigurationInterface();
     170              : 
     171            2 :   std::shared_ptr<ConfigurationBase> cfg1 = std::make_shared<TestConfiguration001>();
     172              : 
     173            2 :   int testVersion = fixture.baseVersion;
     174            2 :   cfg1->getView().version = testVersion;
     175            4 :   cfg1->getView().fillFromJSON("{\"test\": \"first_save\"}");
     176              : 
     177            2 :   std::cout << "Step 1: Saving configuration version " << testVersion << " (first time)...\n";
     178              : 
     179            2 :   auto result1 = ifc.saveActiveVersion(cfg1.get());
     180              : 
     181            2 :   BOOST_CHECK_EQUAL(result1, 0);
     182            2 :   std::cout << "  Result: SUCCESS (as expected)\n";
     183              : 
     184            2 :   std::shared_ptr<ConfigurationBase> cfg2 = std::make_shared<TestConfiguration001>();
     185              : 
     186            2 :   cfg2->getView().version = testVersion;
     187            4 :   cfg2->getView().fillFromJSON("{\"test\": \"second_save\"}");
     188              : 
     189            2 :   std::cout << "Step 2: Attempting to save DUPLICATE version " << testVersion << " (second time)...\n";
     190              : 
     191            2 :   auto result2 = ifc.saveActiveVersion(cfg2.get());
     192              : 
     193            2 :   BOOST_CHECK_NE(result2, 0);
     194            2 :   std::cout << "  Result: FAILURE (as expected - duplicate rejected)\n";
     195              : 
     196            2 :   std::shared_ptr<ConfigurationBase> cfgRead = std::make_shared<TestConfiguration001>();
     197              : 
     198            2 :   auto resultRead = ifc.fill(cfgRead.get(), testVersion);
     199            2 :   BOOST_CHECK_EQUAL(resultRead, 0);
     200              : 
     201           10 :   BOOST_CHECK_MESSAGE(jsonContains(cfgRead->getView()._json, "test", "first_save"), "Expected 'first_save' but got: " << cfgRead->getView()._json);
     202              : 
     203            2 :   std::cout << "  Verified: Original version preserved\n";
     204            2 : }
     205              : 
     206            4 : BOOST_AUTO_TEST_CASE(duplicate_version_allowed_with_overwrite) {
     207            2 :   std::cout << "\n=== TEST 2: Duplicate version allowed with overwrite ===\n";
     208              : 
     209            2 :   auto ifc = DatabaseConfigurationInterface();
     210              : 
     211            2 :   int testVersion = fixture.baseVersion + 1;
     212              : 
     213            2 :   std::shared_ptr<ConfigurationBase> cfg1 = std::make_shared<TestConfiguration001>();
     214            2 :   cfg1->getView().version = testVersion;
     215            4 :   cfg1->getView().fillFromJSON("{\"data\": \"original\"}");
     216              : 
     217            2 :   std::cout << "Step 1: Saving original version " << testVersion << "\n";
     218            2 :   auto result1 = ifc.saveActiveVersion(cfg1.get());
     219            2 :   BOOST_CHECK_EQUAL(result1, 0);
     220              : 
     221            2 :   std::shared_ptr<ConfigurationBase> cfg2 = std::make_shared<TestConfiguration001>();
     222            2 :   cfg2->getView().version = testVersion;
     223            4 :   cfg2->getView().fillFromJSON("{\"data\": \"updated\"}");
     224              : 
     225            2 :   std::cout << "Step 2: Overwriting version " << testVersion << " with overwrite=true\n";
     226            2 :   auto result2 = ifc.saveActiveVersion(cfg2.get(), true);
     227              : 
     228            2 :   BOOST_CHECK_EQUAL(result2, 0);
     229            2 :   std::cout << "  Result: SUCCESS (overwrite allowed)\n";
     230              : 
     231            2 :   std::shared_ptr<ConfigurationBase> cfgRead = std::make_shared<TestConfiguration001>();
     232            2 :   auto resultRead = ifc.fill(cfgRead.get(), testVersion);
     233            2 :   BOOST_CHECK_EQUAL(resultRead, 0);
     234              : 
     235           10 :   BOOST_CHECK_MESSAGE(jsonContains(cfgRead->getView()._json, "data", "updated"), "Expected 'updated' but got: " << cfgRead->getView()._json);
     236            2 :   std::cout << "  Verified: Version successfully overwritten\n";
     237            2 : }
     238              : 
     239            4 : BOOST_AUTO_TEST_CASE(duplicate_composition_rejected) {
     240            2 :   std::cout << "\n=== TEST 3: Duplicate composition rejected ===\n";
     241              : 
     242            2 :   auto ifc = DatabaseConfigurationInterface();
     243              : 
     244            2 :   int version1 = fixture.baseVersion + 10;
     245            2 :   int version2 = fixture.baseVersion + 20;
     246              : 
     247            2 :   std::shared_ptr<ConfigurationBase> cfg1 = std::make_shared<TestConfiguration001>();
     248            2 :   cfg1->getView().version = version1;
     249            2 :   auto result1 = ifc.saveActiveVersion(cfg1.get());
     250            2 :   BOOST_CHECK_EQUAL(result1, 0);
     251              : 
     252            2 :   std::shared_ptr<ConfigurationBase> cfg2 = std::make_shared<TestConfiguration002>();
     253            2 :   cfg2->getView().version = version2;
     254            2 :   auto result2 = ifc.saveActiveVersion(cfg2.get());
     255            2 :   BOOST_CHECK_EQUAL(result2, 0);
     256              : 
     257            2 :   std::cout << "Created versions: " << version1 << ", " << version2 << "\n";
     258              : 
     259            2 :   auto compositionMap = DatabaseConfigurationInterface::config_version_map_t{};
     260            2 :   compositionMap[cfg1->getConfigurationName()] = version1;
     261            2 :   compositionMap[cfg2->getConfigurationName()] = version2;
     262              : 
     263            6 :   std::string compositionName = std::string{"TestComposition_"} + std::to_string(fixture.baseVersion);
     264              : 
     265            2 :   std::cout << "Step 2: Creating composition '" << compositionName << "' (first time)...\n";
     266              : 
     267            2 :   BOOST_CHECK_NO_THROW(ifc.storeGlobalConfiguration(compositionMap, compositionName));
     268            2 :   std::cout << "  Result: SUCCESS\n";
     269              : 
     270            2 :   std::cout << "Step 3: Attempting to create DUPLICATE composition '" << compositionName << "' (second time)...\n";
     271              : 
     272            4 :   BOOST_CHECK_THROW(ifc.storeGlobalConfiguration(compositionMap, compositionName), std::runtime_error);
     273            2 :   std::cout << "  Result: FAILURE (as expected - duplicate rejected)\n";
     274              : 
     275            2 :   auto loadedMap = ifc.loadGlobalConfiguration(compositionName);
     276              : 
     277            2 :   BOOST_CHECK_EQUAL(loadedMap.size(), 2);
     278            2 :   BOOST_CHECK_EQUAL(loadedMap[cfg1->getConfigurationName()], version1);
     279            2 :   BOOST_CHECK_EQUAL(loadedMap[cfg2->getConfigurationName()], version2);
     280              : 
     281            2 :   std::cout << "  Verified: Original composition preserved\n";
     282            2 : }
     283              : 
     284            4 : BOOST_AUTO_TEST_CASE(different_composition_names_allowed) {
     285            2 :   std::cout << "\n=== TEST 4: Different composition names allowed ===\n";
     286              : 
     287            2 :   auto ifc = DatabaseConfigurationInterface();
     288              : 
     289            2 :   int version1 = fixture.baseVersion + 100;
     290            2 :   int version2 = fixture.baseVersion + 200;
     291              : 
     292            2 :   std::shared_ptr<ConfigurationBase> cfg1 = std::make_shared<TestConfiguration001>();
     293            2 :   cfg1->getView().version = version1;
     294            2 :   ifc.saveActiveVersion(cfg1.get());
     295              : 
     296            2 :   std::shared_ptr<ConfigurationBase> cfg2 = std::make_shared<TestConfiguration002>();
     297            2 :   cfg2->getView().version = version2;
     298            2 :   ifc.saveActiveVersion(cfg2.get());
     299              : 
     300            2 :   auto compositionMap = DatabaseConfigurationInterface::config_version_map_t{};
     301            2 :   compositionMap[cfg1->getConfigurationName()] = version1;
     302            2 :   compositionMap[cfg2->getConfigurationName()] = version2;
     303              : 
     304            6 :   std::string compositionName1 = std::string{"Composition_A_"} + std::to_string(fixture.baseVersion);
     305              : 
     306            2 :   std::cout << "Creating composition '" << compositionName1 << "'\n";
     307            2 :   BOOST_CHECK_NO_THROW(ifc.storeGlobalConfiguration(compositionMap, compositionName1));
     308              : 
     309            6 :   std::string compositionName2 = std::string{"Composition_B_"} + std::to_string(fixture.baseVersion);
     310              : 
     311            2 :   std::cout << "Creating composition '" << compositionName2 << "' (same members, different name)\n";
     312              : 
     313            2 :   BOOST_CHECK_NO_THROW(ifc.storeGlobalConfiguration(compositionMap, compositionName2));
     314            2 :   std::cout << "  Result: SUCCESS (different names allowed)\n";
     315              : 
     316            2 :   auto configs = ifc.findAllGlobalConfigurations();
     317              : 
     318            2 :   bool found1 = (std::find(configs.begin(), configs.end(), compositionName1) != configs.end());
     319            2 :   bool found2 = (std::find(configs.begin(), configs.end(), compositionName2) != configs.end());
     320              : 
     321            2 :   BOOST_CHECK_EQUAL(found1, true);
     322            2 :   BOOST_CHECK_EQUAL(found2, true);
     323              : 
     324            2 :   std::cout << "  Verified: Both compositions exist independently\n";
     325            2 : }
     326              : 
     327            4 : BOOST_AUTO_TEST_CASE(composition_different_members_allowed) {
     328            2 :   std::cout << "\n=== TEST 5: Composition different members allowed ===\n";
     329              : 
     330            2 :   auto ifc = DatabaseConfigurationInterface();
     331              : 
     332            2 :   int version1 = fixture.baseVersion + 300;
     333            2 :   int version2 = fixture.baseVersion + 400;
     334            2 :   int version3 = fixture.baseVersion + 500;
     335              : 
     336            2 :   std::shared_ptr<ConfigurationBase> cfg1 = std::make_shared<TestConfiguration001>();
     337            2 :   cfg1->getView().version = version1;
     338            2 :   ifc.saveActiveVersion(cfg1.get());
     339              : 
     340            2 :   std::shared_ptr<ConfigurationBase> cfg2a = std::make_shared<TestConfiguration002>();
     341            2 :   cfg2a->getView().version = version2;
     342            2 :   ifc.saveActiveVersion(cfg2a.get());
     343              : 
     344            2 :   std::shared_ptr<ConfigurationBase> cfg2b = std::make_shared<TestConfiguration002>();
     345            2 :   cfg2b->getView().version = version3;
     346            2 :   ifc.saveActiveVersion(cfg2b.get());
     347              : 
     348            2 :   auto map1 = DatabaseConfigurationInterface::config_version_map_t{};
     349            2 :   map1[cfg1->getConfigurationName()] = version1;
     350            2 :   map1[cfg2a->getConfigurationName()] = version2;
     351              : 
     352            6 :   std::string compName1 = std::string{"CompA_"} + std::to_string(fixture.baseVersion);
     353              : 
     354            2 :   ifc.storeGlobalConfiguration(map1, compName1);
     355            2 :   std::cout << "Created composition '" << compName1 << "' with versions " << version1 << ", " << version2 << "\n";
     356              : 
     357            2 :   auto map2 = DatabaseConfigurationInterface::config_version_map_t{};
     358            2 :   map2[cfg1->getConfigurationName()] = version1;
     359            2 :   map2[cfg2b->getConfigurationName()] = version3;
     360              : 
     361            6 :   std::string compName2 = std::string{"CompB_"} + std::to_string(fixture.baseVersion);
     362              : 
     363            2 :   BOOST_CHECK_NO_THROW(ifc.storeGlobalConfiguration(map2, compName2));
     364            2 :   std::cout << "Created composition '" << compName2 << "' with versions " << version1 << ", " << version3 << " (SUCCESS)\n";
     365            2 : }
     366              : 
     367            4 : BOOST_AUTO_TEST_CASE(raw_storage_verify_no_duplicate) {
     368            2 :   std::cout << "\n=== TEST 6: Raw storage verification ===\n";
     369              : 
     370            2 :   if (!isFileSystemBackend()) {
     371            1 :     std::cout << "  Skipping: Not using FileSystemDB backend\n";
     372            1 :     BOOST_CHECK(true);
     373            1 :     return;
     374              :   }
     375              : 
     376            1 :   auto ifc = DatabaseConfigurationInterface();
     377              : 
     378            1 :   int testVersion = fixture.baseVersion + 600;
     379            1 :   std::string configType = "TestConfiguration001";
     380              : 
     381            1 :   std::shared_ptr<ConfigurationBase> cfg1 = std::make_shared<TestConfiguration001>();
     382            1 :   cfg1->getView().version = testVersion;
     383            2 :   cfg1->getView().fillFromJSON("{\"marker\": \"original_data\"}");
     384              : 
     385            1 :   auto result1 = ifc.saveActiveVersion(cfg1.get());
     386            1 :   BOOST_CHECK_EQUAL(result1, 0);
     387              : 
     388            1 :   std::string collectionPath = fixture.dbPath + "/" + configType;
     389            1 :   int countBefore = countDocumentsWithVersion(collectionPath, configType, std::to_string(testVersion));
     390            1 :   std::cout << "Documents with version " << testVersion << " BEFORE duplicate attempt: " << countBefore << "\n";
     391              : 
     392            1 :   std::shared_ptr<ConfigurationBase> cfg2 = std::make_shared<TestConfiguration001>();
     393            1 :   cfg2->getView().version = testVersion;
     394            2 :   cfg2->getView().fillFromJSON("{\"marker\": \"duplicate_data\"}");
     395              : 
     396            1 :   auto result2 = ifc.saveActiveVersion(cfg2.get());
     397            1 :   BOOST_CHECK_NE(result2, 0);
     398              : 
     399            1 :   int countAfter = countDocumentsWithVersion(collectionPath, configType, std::to_string(testVersion));
     400            1 :   std::cout << "Documents with version " << testVersion << " AFTER duplicate attempt: " << countAfter << "\n";
     401              : 
     402            1 :   BOOST_CHECK_EQUAL(countBefore, countAfter);
     403            1 :   BOOST_CHECK_EQUAL(countAfter, 1);
     404              : 
     405            1 :   std::cout << "  Verified: Duplicate was NOT written to disk\n";
     406            1 : }
     407              : 
     408            4 : BOOST_AUTO_TEST_CASE(duplicate_rejection_preserves_file_count) {
     409            2 :   std::cout << "\n=== TEST 7: Duplicate rejection preserves file count ===\n";
     410              : 
     411            2 :   if (!isFileSystemBackend()) {
     412            1 :     std::cout << "  Skipping: Not using FileSystemDB backend\n";
     413            1 :     BOOST_CHECK(true);
     414            1 :     return;
     415              :   }
     416              : 
     417            1 :   auto ifc = DatabaseConfigurationInterface();
     418              : 
     419            1 :   int testVersion = fixture.baseVersion + 700;
     420            1 :   std::string configType = "TestConfiguration001";
     421            1 :   std::string collectionPath = fixture.dbPath + "/" + configType;
     422              : 
     423            1 :   std::shared_ptr<ConfigurationBase> cfg1 = std::make_shared<TestConfiguration001>();
     424            1 :   cfg1->getView().version = testVersion;
     425            1 :   auto result1 = ifc.saveActiveVersion(cfg1.get());
     426            1 :   BOOST_CHECK_EQUAL(result1, 0);
     427              : 
     428            1 :   auto filesBefore = listJsonFiles(collectionPath);
     429            1 :   int fileCountBefore = filesBefore.size();
     430            1 :   std::cout << "Files in collection BEFORE duplicate attempt: " << fileCountBefore << "\n";
     431              : 
     432            1 :   std::shared_ptr<ConfigurationBase> cfg2 = std::make_shared<TestConfiguration001>();
     433            1 :   cfg2->getView().version = testVersion;
     434            1 :   auto result2 = ifc.saveActiveVersion(cfg2.get());
     435            1 :   BOOST_CHECK_NE(result2, 0);
     436              : 
     437            1 :   auto filesAfter = listJsonFiles(collectionPath);
     438            1 :   int fileCountAfter = filesAfter.size();
     439            1 :   std::cout << "Files in collection AFTER duplicate attempt: " << fileCountAfter << "\n";
     440              : 
     441            1 :   BOOST_CHECK_EQUAL(fileCountBefore, fileCountAfter);
     442              : 
     443            1 :   std::cout << "  Verified: Duplicate rejection did not create extra files\n";
     444            1 : }
     445              : 
     446            4 : BOOST_AUTO_TEST_CASE(stress_test_rapid_duplicates) {
     447            2 :   std::cout << "\n=== TEST 8: Stress test - rapid duplicate attempts ===\n";
     448              : 
     449            2 :   auto ifc = DatabaseConfigurationInterface();
     450              : 
     451            2 :   int testVersion = fixture.baseVersion + 800;
     452            2 :   const int NUM_ATTEMPTS = 10;
     453              : 
     454            2 :   std::shared_ptr<ConfigurationBase> cfgOriginal = std::make_shared<TestConfiguration001>();
     455            2 :   cfgOriginal->getView().version = testVersion;
     456            2 :   auto result = ifc.saveActiveVersion(cfgOriginal.get());
     457            2 :   BOOST_CHECK_EQUAL(result, 0);
     458              : 
     459            2 :   std::cout << "Original version " << testVersion << " saved\n";
     460              : 
     461            2 :   auto startTime = std::chrono::high_resolution_clock::now();
     462              : 
     463            2 :   int successCount = 0;
     464            2 :   int failCount = 0;
     465              : 
     466           22 :   for (int i = 0; i < NUM_ATTEMPTS; i++) {
     467           20 :     std::shared_ptr<ConfigurationBase> cfgDup = std::make_shared<TestConfiguration001>();
     468           20 :     cfgDup->getView().version = testVersion;
     469           20 :     cfgDup->getView().fillFromJSON("{\"attempt\": " + std::to_string(i) + "}");
     470              : 
     471           20 :     auto dupResult = ifc.saveActiveVersion(cfgDup.get());
     472           20 :     if (dupResult == 0) {
     473            0 :       successCount++;
     474              :     } else {
     475           20 :       failCount++;
     476              :     }
     477           20 :   }
     478              : 
     479            2 :   auto endTime = std::chrono::high_resolution_clock::now();
     480            2 :   auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);
     481              : 
     482            2 :   std::cout << "  Completed " << NUM_ATTEMPTS << " duplicate attempts in " << duration.count() << "ms\n";
     483            2 :   std::cout << "  Success: " << successCount << ", Failed: " << failCount << "\n";
     484              : 
     485            2 :   BOOST_CHECK_EQUAL(successCount, 0);
     486            2 :   BOOST_CHECK_EQUAL(failCount, NUM_ATTEMPTS);
     487              : 
     488            2 :   std::shared_ptr<ConfigurationBase> cfgRead = std::make_shared<TestConfiguration001>();
     489            2 :   auto readResult = ifc.fill(cfgRead.get(), testVersion);
     490            2 :   BOOST_CHECK_EQUAL(readResult, 0);
     491              : 
     492            2 :   std::cout << "  Verified: Original data preserved after " << NUM_ATTEMPTS << " duplicate attempts\n";
     493            2 : }
     494              : 
     495            4 : BOOST_AUTO_TEST_CASE(edge_case_boundary_versions) {
     496            2 :   std::cout << "\n=== TEST 9: Edge case - boundary versions ===\n";
     497              : 
     498            2 :   auto ifc = DatabaseConfigurationInterface();
     499              : 
     500            2 :   int lowVersion = fixture.baseVersion + 900;
     501            2 :   int highVersion = fixture.baseVersion + 9999;
     502              : 
     503            2 :   std::shared_ptr<ConfigurationBase> cfg1 = std::make_shared<TestConfiguration001>();
     504            2 :   cfg1->getView().version = lowVersion;
     505              : 
     506            2 :   bool v1Saved = (ifc.saveActiveVersion(cfg1.get()) == 0);
     507            2 :   std::cout << "  Version " << lowVersion << ": " << (v1Saved ? "SUCCESS" : "FAILED") << "\n";
     508            2 :   BOOST_CHECK_EQUAL(v1Saved, true);
     509              : 
     510            2 :   std::shared_ptr<ConfigurationBase> cfg1dup = std::make_shared<TestConfiguration001>();
     511            2 :   cfg1dup->getView().version = lowVersion;
     512              : 
     513            2 :   bool v1DupRejected = (ifc.saveActiveVersion(cfg1dup.get()) != 0);
     514            2 :   std::cout << "  Version " << lowVersion << " duplicate: " << (v1DupRejected ? "REJECTED (good)" : "ACCEPTED (bad)") << "\n";
     515              : 
     516            2 :   BOOST_CHECK_EQUAL(v1DupRejected, true);
     517              : 
     518            2 :   std::shared_ptr<ConfigurationBase> cfgLarge = std::make_shared<TestConfiguration001>();
     519            2 :   cfgLarge->getView().version = highVersion;
     520              : 
     521            2 :   bool largeSaved = (ifc.saveActiveVersion(cfgLarge.get()) == 0);
     522            2 :   std::cout << "  Version " << highVersion << ": " << (largeSaved ? "SUCCESS" : "FAILED") << "\n";
     523            2 :   BOOST_CHECK_EQUAL(largeSaved, true);
     524              : 
     525            2 :   std::shared_ptr<ConfigurationBase> cfgLargeDup = std::make_shared<TestConfiguration001>();
     526            2 :   cfgLargeDup->getView().version = highVersion;
     527              : 
     528            2 :   bool largeDupRejected = (ifc.saveActiveVersion(cfgLargeDup.get()) != 0);
     529            2 :   std::cout << "  Version " << highVersion << " duplicate: " << (largeDupRejected ? "REJECTED (good)" : "ACCEPTED (bad)") << "\n";
     530              : 
     531            2 :   BOOST_CHECK_EQUAL(largeDupRejected, true);
     532              : 
     533            2 :   std::cout << "  Verified: Boundary version duplicate detection works\n";
     534            2 : }
     535              : 
     536            4 : BOOST_AUTO_TEST_CASE(edge_case_special_characters) {
     537            2 :   std::cout << "\n=== TEST 10: Edge case - special characters in data ===\n";
     538              : 
     539            2 :   auto ifc = DatabaseConfigurationInterface();
     540              : 
     541            2 :   int testVersion = fixture.baseVersion + 1000;
     542              : 
     543            2 :   std::shared_ptr<ConfigurationBase> cfg1 = std::make_shared<TestConfiguration001>();
     544            2 :   cfg1->getView().version = testVersion;
     545            4 :   cfg1->getView().fillFromJSON("{\"special\": \"line1\\nline2\\ttab\"}");
     546              : 
     547            2 :   auto result1 = ifc.saveActiveVersion(cfg1.get());
     548            2 :   BOOST_CHECK_EQUAL(result1, 0);
     549            2 :   std::cout << "  Saved version with special characters\n";
     550              : 
     551            2 :   std::shared_ptr<ConfigurationBase> cfg2 = std::make_shared<TestConfiguration001>();
     552            2 :   cfg2->getView().version = testVersion;
     553            4 :   cfg2->getView().fillFromJSON("{\"special\": \"different\\ndata\"}");
     554              : 
     555            2 :   auto result2 = ifc.saveActiveVersion(cfg2.get());
     556            2 :   BOOST_CHECK_NE(result2, 0);
     557            2 :   std::cout << "  Duplicate with different special chars: REJECTED (good)\n";
     558              : 
     559            2 :   std::shared_ptr<ConfigurationBase> cfgRead = std::make_shared<TestConfiguration001>();
     560            2 :   ifc.fill(cfgRead.get(), testVersion);
     561              : 
     562            2 :   BOOST_CHECK_MESSAGE(cfgRead->getView()._json.find("line1") != std::string::npos, "Original data should be preserved");
     563              : 
     564            2 :   std::cout << "  Verified: Special character handling works correctly\n";
     565            2 : }
     566              : 
     567            4 : BOOST_AUTO_TEST_CASE(performance_duplicate_detection) {
     568            2 :   std::cout << "\n=== TEST 11: Performance measurement ===\n";
     569              : 
     570            2 :   auto ifc = DatabaseConfigurationInterface();
     571              : 
     572            2 :   const int NUM_VERSIONS = 10;
     573            2 :   int baseVer = fixture.baseVersion + 1100;
     574              : 
     575            2 :   auto startUnique = std::chrono::high_resolution_clock::now();
     576              : 
     577           22 :   for (int i = 0; i < NUM_VERSIONS; i++) {
     578           20 :     std::shared_ptr<ConfigurationBase> cfg = std::make_shared<TestConfiguration001>();
     579           20 :     cfg->getView().version = baseVer + i;
     580           20 :     ifc.saveActiveVersion(cfg.get());
     581           20 :   }
     582              : 
     583            2 :   auto endUnique = std::chrono::high_resolution_clock::now();
     584            2 :   auto uniqueDuration = std::chrono::duration_cast<std::chrono::milliseconds>(endUnique - startUnique);
     585              : 
     586            2 :   std::cout << "  Time to save " << NUM_VERSIONS << " unique versions: " << uniqueDuration.count() << "ms\n";
     587            2 :   std::cout << "  Average per version: " << (uniqueDuration.count() / NUM_VERSIONS) << "ms\n";
     588              : 
     589            2 :   auto startDup = std::chrono::high_resolution_clock::now();
     590              : 
     591           22 :   for (int i = 0; i < NUM_VERSIONS; i++) {
     592           20 :     std::shared_ptr<ConfigurationBase> cfg = std::make_shared<TestConfiguration001>();
     593           20 :     cfg->getView().version = baseVer + i;
     594           20 :     ifc.saveActiveVersion(cfg.get());
     595           20 :   }
     596              : 
     597            2 :   auto endDup = std::chrono::high_resolution_clock::now();
     598            2 :   auto dupDuration = std::chrono::duration_cast<std::chrono::milliseconds>(endDup - startDup);
     599              : 
     600            2 :   std::cout << "  Time to reject " << NUM_VERSIONS << " duplicates: " << dupDuration.count() << "ms\n";
     601            2 :   std::cout << "  Average per rejection: " << (dupDuration.count() / NUM_VERSIONS) << "ms\n";
     602              : 
     603            2 :   BOOST_CHECK_LT(uniqueDuration.count() + dupDuration.count(), 10000);
     604              : 
     605            2 :   std::cout << "  Verified: Performance within acceptable limits\n";
     606            2 : }
     607              : 
     608            4 : BOOST_AUTO_TEST_CASE(composition_overwrite_allowed) {
     609            2 :   std::cout << "\n=== TEST 12: Composition overwrite flag test ===\n";
     610              : 
     611            2 :   auto ifc = DatabaseConfigurationInterface();
     612              : 
     613            2 :   int version1 = fixture.baseVersion + 1200;
     614            2 :   int version2 = fixture.baseVersion + 1201;
     615            2 :   int version3 = fixture.baseVersion + 1202;
     616              : 
     617            2 :   std::shared_ptr<ConfigurationBase> cfg1 = std::make_shared<TestConfiguration001>();
     618            2 :   cfg1->getView().version = version1;
     619            2 :   auto r1 = ifc.saveActiveVersion(cfg1.get());
     620            2 :   BOOST_CHECK_EQUAL(r1, 0);
     621              : 
     622            2 :   std::shared_ptr<ConfigurationBase> cfg2 = std::make_shared<TestConfiguration002>();
     623            2 :   cfg2->getView().version = version2;
     624            2 :   auto r2 = ifc.saveActiveVersion(cfg2.get());
     625            2 :   BOOST_CHECK_EQUAL(r2, 0);
     626              : 
     627            2 :   std::shared_ptr<ConfigurationBase> cfg3 = std::make_shared<TestConfiguration002>();
     628            2 :   cfg3->getView().version = version3;
     629            2 :   auto r3 = ifc.saveActiveVersion(cfg3.get());
     630            2 :   BOOST_CHECK_EQUAL(r3, 0);
     631              : 
     632            2 :   auto map1 = DatabaseConfigurationInterface::config_version_map_t{};
     633            2 :   map1[cfg1->getConfigurationName()] = version1;
     634            2 :   map1[cfg2->getConfigurationName()] = version2;
     635              : 
     636            6 :   std::string compName = std::string{"OverwriteComp_"} + std::to_string(fixture.baseVersion);
     637              : 
     638            2 :   bool originalCreated = false;
     639              :   try {
     640            2 :     ifc.storeGlobalConfiguration(map1, compName);
     641            2 :     originalCreated = true;
     642            2 :     std::cout << "Step 1: Created composition '" << compName << "' with v" << version1 << ", v" << version2 << "\n";
     643            0 :   } catch (std::exception const& e) {
     644            0 :     std::cout << "  Failed to create original composition: " << e.what() << "\n";
     645            0 :   }
     646            2 :   BOOST_CHECK_EQUAL(originalCreated, true);
     647              : 
     648            2 :   bool duplicateFailed = false;
     649              :   try {
     650            2 :     ifc.storeGlobalConfiguration(map1, compName, false);
     651            0 :     std::cout << "  ERROR: Duplicate composition was allowed without overwrite flag\n";
     652            2 :   } catch (std::exception const& e) {
     653            2 :     duplicateFailed = true;
     654            2 :     std::cout << "Step 2: Duplicate without overwrite rejected (as expected)\n";
     655            2 :   }
     656            2 :   BOOST_CHECK_EQUAL(duplicateFailed, true);
     657              : 
     658            2 :   auto map2 = DatabaseConfigurationInterface::config_version_map_t{};
     659            2 :   map2[cfg1->getConfigurationName()] = version1;
     660            2 :   map2[cfg3->getConfigurationName()] = version3;
     661              : 
     662            2 :   bool overwriteSucceeded = false;
     663              :   try {
     664            2 :     ifc.storeGlobalConfiguration(map2, compName, true);
     665            2 :     overwriteSucceeded = true;
     666            2 :     std::cout << "Step 3: Overwrite with allowOverwrite=true succeeded\n";
     667            0 :   } catch (std::exception const& e) {
     668            0 :     std::cout << "  Overwrite failed: " << e.what() << "\n";
     669            0 :   }
     670            2 :   BOOST_CHECK_EQUAL(overwriteSucceeded, true);
     671              : 
     672            2 :   std::cout << "  Verified: allowOverwrite flag controls composition update behavior\n";
     673            2 : }
     674              : 
     675            4 : BOOST_AUTO_TEST_CASE(sequential_multi_version_duplicates) {
     676            2 :   std::cout << "\n=== TEST 13: Sequential multi-version with duplicate checks ===\n";
     677              : 
     678            2 :   auto ifc = DatabaseConfigurationInterface();
     679              : 
     680            2 :   const int NUM_VERSIONS = 5;
     681            2 :   int baseVer = fixture.baseVersion + 1300;
     682              : 
     683            2 :   auto startTime = std::chrono::high_resolution_clock::now();
     684              : 
     685           12 :   for (int i = 0; i < NUM_VERSIONS; i++) {
     686           10 :     int version = baseVer + i;
     687              : 
     688           10 :     std::shared_ptr<ConfigurationBase> cfgOrig = std::make_shared<TestConfiguration001>();
     689           10 :     cfgOrig->getView().version = version;
     690           10 :     auto origResult = ifc.saveActiveVersion(cfgOrig.get());
     691           10 :     BOOST_CHECK_EQUAL(origResult, 0);
     692              : 
     693           10 :     std::shared_ptr<ConfigurationBase> cfgDup = std::make_shared<TestConfiguration001>();
     694           10 :     cfgDup->getView().version = version;
     695           10 :     auto dupResult = ifc.saveActiveVersion(cfgDup.get());
     696           10 :     BOOST_CHECK_NE(dupResult, 0);
     697           10 :   }
     698              : 
     699            2 :   auto endTime = std::chrono::high_resolution_clock::now();
     700            2 :   auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);
     701              : 
     702            2 :   std::cout << "  Created " << NUM_VERSIONS << " versions with immediate duplicate checks\n";
     703            2 :   std::cout << "  Total time: " << duration.count() << "ms\n";
     704              : 
     705            2 :   auto versions = ifc.getVersions(std::make_shared<TestConfiguration001>().get());
     706            2 :   int foundCount = 0;
     707           12 :   for (int i = 0; i < NUM_VERSIONS; i++) {
     708           10 :     if (versions.find(baseVer + i) != versions.end()) {
     709           10 :       foundCount++;
     710              :     }
     711              :   }
     712              : 
     713            2 :   BOOST_CHECK_EQUAL(foundCount, NUM_VERSIONS);
     714            2 :   std::cout << "  Verified: All " << NUM_VERSIONS << " unique versions exist\n";
     715            2 : }
     716              : 
     717            4 : BOOST_AUTO_TEST_CASE(comprehensive_duplicate_scan) {
     718            2 :   std::cout << "\n=== TEST 14: Comprehensive duplicate scan ===\n";
     719              : 
     720            2 :   if (!isFileSystemBackend()) {
     721            1 :     std::cout << "  Skipping: Not using FileSystemDB backend\n";
     722            1 :     BOOST_CHECK(true);
     723            1 :     return;
     724              :   }
     725              : 
     726            1 :   std::string configType = "TestConfiguration001";
     727            1 :   std::string collectionPath = fixture.dbPath + "/" + configType;
     728              : 
     729            1 :   auto jsonFiles = listJsonFiles(collectionPath);
     730            1 :   std::cout << "  Scanning " << jsonFiles.size() << " files in " << collectionPath << "\n";
     731              : 
     732            1 :   std::map<std::string, int> versionCounts;
     733            1 :   std::regex versionPattern("\"version\"\\s*:\\s*\"([^\"]+)\"");
     734              : 
     735           28 :   for (const auto& filepath : jsonFiles) {
     736           27 :     std::string content = readFileContent(filepath);
     737           27 :     if (content.empty()) continue;
     738              : 
     739           27 :     std::smatch match;
     740           27 :     if (std::regex_search(content, match, versionPattern)) {
     741           27 :       std::string version = match[1].str();
     742           27 :       versionCounts[version]++;
     743           27 :     }
     744           27 :   }
     745              : 
     746            1 :   int duplicatesFound = 0;
     747           28 :   for (const auto& entry : versionCounts) {
     748           27 :     if (entry.second > 1) {
     749            0 :       std::cout << "  DUPLICATE: Version " << entry.first << " appears " << entry.second << " times\n";
     750            0 :       duplicatesFound++;
     751              :     }
     752              :   }
     753              : 
     754            1 :   std::cout << "  Versions scanned: " << versionCounts.size() << "\n";
     755            1 :   std::cout << "  Duplicates found: " << duplicatesFound << "\n";
     756              : 
     757            1 :   BOOST_CHECK_EQUAL(duplicatesFound, 0);
     758            1 :   std::cout << "  Verified: No duplicate versions in storage\n";
     759            1 : }
     760              : 
     761              : BOOST_AUTO_TEST_SUITE_END()
        

Generated by: LCOV version 2.0-1