Line data Source code
1 : #define BOOST_TEST_DYN_LINK
2 : #define BOOST_TEST_MODULE (ThreeVersionsBug test)
3 :
4 : #include <boost/test/unit_test.hpp>
5 :
6 : #include <dirent.h>
7 : #include <atomic>
8 : #include <chrono>
9 : #include <fstream>
10 : #include <functional>
11 : #include <iostream>
12 : #include <map>
13 : #include <memory>
14 : #include <mutex>
15 : #include <regex>
16 : #include <set>
17 : #include <sstream>
18 : #include <string>
19 : #include <thread>
20 : #include <vector>
21 :
22 : #include "../DatabaseConfigurationInterface.h"
23 :
24 : using namespace ots;
25 :
26 : struct BugTestData {
27 2 : BugTestData() {
28 2 : srand(time(nullptr));
29 2 : baseVersion = rand() % 99999 + 100000;
30 2 : std::cout << "=== INVESTIGATING THREE VERSIONS BUG ===\n";
31 2 : std::cout << "Base version: " << baseVersion << "\n\n";
32 :
33 2 : auto uri = std::getenv("ARTDAQ_DATABASE_URI");
34 2 : if (uri) {
35 2 : std::string uriStr(uri);
36 2 : if (uriStr.find("filesystemdb:///") == 0) {
37 1 : dbPath = uriStr.substr(16);
38 1 : } else if (uriStr.find("filesystemdb://") == 0) {
39 0 : dbPath = uriStr.substr(15);
40 : } else {
41 1 : dbPath = uriStr;
42 : }
43 2 : std::cout << "Database path: " << dbPath << "\n";
44 2 : }
45 2 : }
46 :
47 2 : ~BugTestData() { std::cout << "\n=== INVESTIGATION COMPLETE ===\n"; }
48 :
49 : int baseVersion;
50 : std::string dbPath;
51 : };
52 :
53 : BugTestData fixture;
54 :
55 1 : std::string readFileContent(const std::string& filepath) {
56 1 : std::ifstream file(filepath);
57 1 : if (!file.is_open()) {
58 2 : return "";
59 : }
60 0 : std::stringstream buffer;
61 0 : buffer << file.rdbuf();
62 0 : return buffer.str();
63 1 : }
64 :
65 6 : std::vector<std::string> listJsonFiles(const std::string& dirPath) {
66 6 : std::vector<std::string> files;
67 :
68 6 : DIR* dir = opendir(dirPath.c_str());
69 6 : if (dir == nullptr) {
70 6 : return files;
71 : }
72 :
73 : struct dirent* entry;
74 0 : while ((entry = readdir(dir)) != nullptr) {
75 0 : std::string name = entry->d_name;
76 0 : if (name.size() > 5 && name.substr(name.size() - 5) == ".json" && name != "index.json") {
77 0 : files.push_back(dirPath + "/" + name);
78 : }
79 0 : }
80 0 : closedir(dir);
81 0 : return files;
82 0 : }
83 :
84 0 : int countConfigTypeOccurrences(const std::string& jsonContent, const std::string& configType) {
85 0 : std::regex pattern("\"name\"\\s*:\\s*\"" + configType + "\"");
86 :
87 0 : auto begin = std::sregex_iterator(jsonContent.begin(), jsonContent.end(), pattern);
88 0 : auto end = std::sregex_iterator();
89 :
90 0 : return std::distance(begin, end);
91 0 : }
92 :
93 0 : std::vector<std::string> extractConfigNames(const std::string& jsonContent) {
94 0 : std::vector<std::string> names;
95 0 : std::regex pattern("\"name\"\\s*:\\s*\"([^\"]+)\"");
96 :
97 0 : auto begin = std::sregex_iterator(jsonContent.begin(), jsonContent.end(), pattern);
98 0 : auto end = std::sregex_iterator();
99 :
100 0 : for (auto it = begin; it != end; ++it) {
101 0 : names.push_back((*it)[1].str());
102 0 : }
103 0 : return names;
104 0 : }
105 :
106 4 : bool isFileSystemBackend() {
107 4 : auto uri = std::getenv("ARTDAQ_DATABASE_URI");
108 4 : if (uri) {
109 4 : std::string uriStr(uri);
110 4 : return uriStr.find("filesystemdb") != std::string::npos;
111 4 : }
112 0 : return false;
113 : }
114 :
115 1 : std::set<std::string> extractOidsFromIndex(const std::string& indexPath) {
116 1 : std::set<std::string> oids;
117 1 : std::string content = readFileContent(indexPath);
118 1 : if (content.empty()) {
119 1 : return oids;
120 : }
121 :
122 0 : std::regex pattern("\"oid\"\\s*:\\s*\"([a-f0-9]+)\"");
123 :
124 0 : auto begin = std::sregex_iterator(content.begin(), content.end(), pattern);
125 0 : auto end = std::sregex_iterator();
126 :
127 0 : for (auto it = begin; it != end; ++it) {
128 0 : oids.insert((*it)[1].str());
129 0 : }
130 0 : return oids;
131 1 : }
132 :
133 0 : std::string getOidFromFilename(const std::string& filepath) {
134 0 : size_t lastSlash = filepath.rfind('/');
135 0 : std::string filename = (lastSlash != std::string::npos) ? filepath.substr(lastSlash + 1) : filepath;
136 :
137 0 : if (filename.size() > 5 && filename.substr(filename.size() - 5) == ".json") {
138 0 : return filename.substr(0, filename.size() - 5);
139 : }
140 0 : return "";
141 0 : }
142 :
143 : BOOST_AUTO_TEST_SUITE(three_versions_bug_test)
144 :
145 4 : BOOST_AUTO_TEST_CASE(attempt_three_versions_same_type) {
146 2 : std::cout << "\n=== TEST 1: Attempting to add three versions ===\n";
147 :
148 2 : auto ifc = DatabaseConfigurationInterface();
149 :
150 2 : int version1 = fixture.baseVersion;
151 2 : int version2 = fixture.baseVersion + 1;
152 2 : int version3 = fixture.baseVersion + 2;
153 :
154 2 : std::shared_ptr<ConfigurationBase> cfg1 = std::make_shared<TestConfiguration001>();
155 2 : cfg1->getView().version = version1;
156 2 : ifc.saveActiveVersion(cfg1.get());
157 2 : std::cout << "Created version 1: " << version1 << "\n";
158 :
159 2 : std::shared_ptr<ConfigurationBase> cfg2 = std::make_shared<TestConfiguration001>();
160 2 : cfg2->getView().version = version2;
161 2 : ifc.saveActiveVersion(cfg2.get());
162 2 : std::cout << "Created version 2: " << version2 << "\n";
163 :
164 2 : std::shared_ptr<ConfigurationBase> cfg3 = std::make_shared<TestConfiguration001>();
165 2 : cfg3->getView().version = version3;
166 2 : ifc.saveActiveVersion(cfg3.get());
167 2 : std::cout << "Created version 3: " << version3 << "\n";
168 :
169 2 : std::cout << "\nAttempt 1: Using std::map...\n";
170 2 : auto compositionMap = DatabaseConfigurationInterface::config_version_map_t{};
171 :
172 2 : compositionMap[cfg1->getConfigurationName()] = version1;
173 2 : std::cout << " Added version1, map size: " << compositionMap.size() << "\n";
174 :
175 2 : compositionMap[cfg2->getConfigurationName()] = version2;
176 2 : std::cout << " Added version2, map size: " << compositionMap.size() << " (should still be 1)\n";
177 :
178 2 : compositionMap[cfg3->getConfigurationName()] = version3;
179 2 : std::cout << " Added version3, map size: " << compositionMap.size() << " (should still be 1)\n";
180 :
181 2 : BOOST_CHECK_EQUAL(compositionMap.size(), 1);
182 2 : BOOST_CHECK_EQUAL(compositionMap[cfg1->getConfigurationName()], version3);
183 :
184 2 : std::cout << " Result: Map correctly contains only last version: " << version3 << "\n";
185 :
186 6 : std::string compName = std::string{"BugTest1_"} + std::to_string(fixture.baseVersion);
187 :
188 2 : ifc.storeGlobalConfiguration(compositionMap, compName);
189 2 : std::cout << " Created composition: " << compName << "\n";
190 :
191 2 : auto loadedMap = ifc.loadGlobalConfiguration(compName);
192 2 : BOOST_CHECK_EQUAL(loadedMap.size(), 1);
193 :
194 2 : std::cout << " Verified: Composition has " << loadedMap.size() << " member (correct)\n";
195 2 : }
196 :
197 4 : BOOST_AUTO_TEST_CASE(inspect_database_storage_format) {
198 2 : std::cout << "\n=== TEST 2: Inspecting database storage format ===\n";
199 :
200 2 : auto ifc = DatabaseConfigurationInterface();
201 :
202 2 : int version1 = fixture.baseVersion + 10;
203 2 : int version2 = fixture.baseVersion + 11;
204 :
205 2 : std::shared_ptr<ConfigurationBase> cfg1 = std::make_shared<TestConfiguration001>();
206 2 : cfg1->getView().version = version1;
207 2 : ifc.saveActiveVersion(cfg1.get());
208 :
209 2 : std::shared_ptr<ConfigurationBase> cfg2 = std::make_shared<TestConfiguration002>();
210 2 : cfg2->getView().version = version2;
211 2 : ifc.saveActiveVersion(cfg2.get());
212 :
213 2 : auto compositionMap = DatabaseConfigurationInterface::config_version_map_t{};
214 2 : compositionMap[cfg1->getConfigurationName()] = version1;
215 2 : compositionMap[cfg2->getConfigurationName()] = version2;
216 :
217 6 : std::string compName = std::string{"BugTest2_"} + std::to_string(fixture.baseVersion);
218 :
219 2 : ifc.storeGlobalConfiguration(compositionMap, compName);
220 :
221 2 : auto loadedMap = ifc.loadGlobalConfiguration(compName);
222 :
223 2 : std::cout << "Composition '" << compName << "' contains:\n";
224 6 : for (auto const& entry : loadedMap) {
225 4 : std::cout << " " << entry.first << " -> version " << entry.second << "\n";
226 : }
227 :
228 2 : BOOST_CHECK_EQUAL(loadedMap.size(), 2);
229 :
230 2 : int count001 = loadedMap.count(cfg1->getConfigurationName());
231 2 : int count002 = loadedMap.count(cfg2->getConfigurationName());
232 :
233 2 : BOOST_CHECK_EQUAL(count001, 1);
234 2 : BOOST_CHECK_EQUAL(count002, 1);
235 :
236 2 : std::cout << " Verified: Each configuration type appears exactly once\n";
237 2 : }
238 :
239 4 : BOOST_AUTO_TEST_CASE(verify_raw_storage_no_duplicates) {
240 2 : std::cout << "\n=== TEST 3: Verifying raw storage has no duplicates ===\n";
241 :
242 2 : auto ifc = DatabaseConfigurationInterface();
243 :
244 2 : int version1 = fixture.baseVersion + 20;
245 2 : int version2 = fixture.baseVersion + 21;
246 :
247 2 : std::shared_ptr<ConfigurationBase> cfg1 = std::make_shared<TestConfiguration001>();
248 2 : cfg1->getView().version = version1;
249 2 : ifc.saveActiveVersion(cfg1.get());
250 :
251 2 : std::shared_ptr<ConfigurationBase> cfg2 = std::make_shared<TestConfiguration002>();
252 2 : cfg2->getView().version = version2;
253 2 : ifc.saveActiveVersion(cfg2.get());
254 :
255 2 : auto compositionMap = DatabaseConfigurationInterface::config_version_map_t{};
256 2 : compositionMap[cfg1->getConfigurationName()] = version1;
257 2 : compositionMap[cfg2->getConfigurationName()] = version2;
258 :
259 6 : std::string compName = std::string{"RawStorageTest_"} + std::to_string(fixture.baseVersion);
260 2 : ifc.storeGlobalConfiguration(compositionMap, compName);
261 :
262 2 : std::cout << "Created composition: " << compName << "\n";
263 :
264 2 : std::string globalConfigDir = fixture.dbPath + "/GlobalConfigurations";
265 2 : std::cout << "Scanning: " << globalConfigDir << "\n";
266 :
267 2 : auto jsonFiles = listJsonFiles(globalConfigDir);
268 2 : std::cout << "Found " << jsonFiles.size() << " JSON files\n";
269 :
270 2 : bool foundOurComposition = false;
271 2 : int totalDuplicatesFound = 0;
272 :
273 2 : for (const auto& filepath : jsonFiles) {
274 0 : std::string content = readFileContent(filepath);
275 0 : if (content.empty()) {
276 0 : continue;
277 : }
278 :
279 0 : if (content.find(compName) != std::string::npos) {
280 0 : foundOurComposition = true;
281 0 : std::cout << "\nFound our composition in: " << filepath << "\n";
282 :
283 0 : auto configNames = extractConfigNames(content);
284 0 : std::cout << " Configuration entries found: " << configNames.size() << "\n";
285 :
286 0 : std::set<std::string> uniqueNames;
287 0 : std::map<std::string, int> nameCounts;
288 :
289 0 : for (const auto& name : configNames) {
290 0 : nameCounts[name]++;
291 0 : uniqueNames.insert(name);
292 : }
293 :
294 0 : for (const auto& entry : nameCounts) {
295 0 : if (entry.second > 1) {
296 0 : std::cout << " DUPLICATE DETECTED: '" << entry.first << "' appears " << entry.second << " times!\n";
297 0 : totalDuplicatesFound++;
298 : }
299 : }
300 :
301 0 : int count001 = countConfigTypeOccurrences(content, cfg1->getConfigurationName());
302 0 : int count002 = countConfigTypeOccurrences(content, cfg2->getConfigurationName());
303 :
304 0 : std::cout << " " << cfg1->getConfigurationName() << " appears " << count001 << " time(s)\n";
305 0 : std::cout << " " << cfg2->getConfigurationName() << " appears " << count002 << " time(s)\n";
306 :
307 0 : BOOST_CHECK_EQUAL(count001, 1);
308 0 : BOOST_CHECK_EQUAL(count002, 1);
309 0 : }
310 0 : }
311 :
312 2 : BOOST_CHECK_MESSAGE(foundOurComposition || jsonFiles.empty(), "Composition should be found in raw storage (or directory doesn't exist)");
313 :
314 2 : BOOST_CHECK_EQUAL(totalDuplicatesFound, 0);
315 :
316 2 : std::cout << "\n Verified: Raw storage contains no duplicate configuration types\n";
317 2 : }
318 :
319 4 : BOOST_AUTO_TEST_CASE(scan_all_compositions_for_duplicates) {
320 2 : std::cout << "\n=== TEST 4: Scanning all compositions for duplicate entries ===\n";
321 :
322 2 : std::string globalConfigDir = fixture.dbPath + "/GlobalConfigurations";
323 2 : auto jsonFiles = listJsonFiles(globalConfigDir);
324 :
325 2 : std::cout << "Scanning " << jsonFiles.size() << " composition files in: " << globalConfigDir << "\n";
326 :
327 2 : int filesWithDuplicates = 0;
328 2 : int totalFilesScanned = 0;
329 :
330 2 : for (const auto& filepath : jsonFiles) {
331 0 : std::string content = readFileContent(filepath);
332 0 : if (content.empty()) {
333 0 : continue;
334 : }
335 :
336 0 : totalFilesScanned++;
337 :
338 0 : auto configNames = extractConfigNames(content);
339 :
340 0 : std::map<std::string, int> nameCounts;
341 0 : for (const auto& name : configNames) {
342 0 : nameCounts[name]++;
343 : }
344 :
345 0 : bool hasDuplicates = false;
346 0 : for (const auto& entry : nameCounts) {
347 0 : if (entry.second > 1) {
348 0 : if (!hasDuplicates) {
349 0 : std::cout << "\nDuplicates in: " << filepath << "\n";
350 0 : hasDuplicates = true;
351 0 : filesWithDuplicates++;
352 : }
353 0 : std::cout << " '" << entry.first << "' appears " << entry.second << " times\n";
354 : }
355 : }
356 0 : }
357 :
358 2 : std::cout << "\nSummary:\n";
359 2 : std::cout << " Files scanned: " << totalFilesScanned << "\n";
360 2 : std::cout << " Files with duplicates: " << filesWithDuplicates << "\n";
361 :
362 2 : BOOST_CHECK_EQUAL(filesWithDuplicates, 0);
363 :
364 2 : std::cout << "\n Verified: No compositions contain duplicate configuration types\n";
365 2 : }
366 :
367 4 : BOOST_AUTO_TEST_CASE(map_interface_prevents_duplicates) {
368 2 : std::cout << "\n=== TEST 5: Map interface prevents duplicates ===\n";
369 :
370 2 : auto compositionMap = DatabaseConfigurationInterface::config_version_map_t{};
371 :
372 2 : std::string typeName = "TestConfiguration001";
373 :
374 2 : compositionMap[typeName] = 100;
375 2 : std::cout << "Added version 100, map size: " << compositionMap.size() << "\n";
376 2 : BOOST_CHECK_EQUAL(compositionMap.size(), 1);
377 2 : BOOST_CHECK_EQUAL(compositionMap[typeName], 100);
378 :
379 2 : compositionMap[typeName] = 200;
380 2 : std::cout << "Added version 200, map size: " << compositionMap.size() << "\n";
381 2 : BOOST_CHECK_EQUAL(compositionMap.size(), 1);
382 2 : BOOST_CHECK_EQUAL(compositionMap[typeName], 200);
383 :
384 2 : compositionMap[typeName] = 300;
385 2 : std::cout << "Added version 300, map size: " << compositionMap.size() << "\n";
386 2 : BOOST_CHECK_EQUAL(compositionMap.size(), 1);
387 2 : BOOST_CHECK_EQUAL(compositionMap[typeName], 300);
388 :
389 2 : std::cout << " Verified: std::map automatically prevents duplicates\n";
390 2 : std::cout << " Final value: " << compositionMap[typeName] << " (should be 300)\n";
391 2 : }
392 :
393 4 : BOOST_AUTO_TEST_CASE(test_duplicate_composition_behavior) {
394 2 : std::cout << "\n=== TEST 6: Test duplicate composition behavior ===\n";
395 :
396 2 : auto ifc = DatabaseConfigurationInterface();
397 :
398 2 : int version1 = fixture.baseVersion + 1000;
399 :
400 2 : std::shared_ptr<ConfigurationBase> cfg1 = std::make_shared<TestConfiguration001>();
401 2 : cfg1->getView().version = version1;
402 2 : ifc.saveActiveVersion(cfg1.get());
403 :
404 6 : std::string compName = std::string{"DupCompTest_"} + std::to_string(fixture.baseVersion);
405 :
406 2 : auto map1 = DatabaseConfigurationInterface::config_version_map_t{};
407 2 : map1[cfg1->getConfigurationName()] = version1;
408 :
409 2 : ifc.storeGlobalConfiguration(map1, compName);
410 2 : std::cout << "Created initial composition: " << compName << "\n";
411 :
412 2 : bool secondWriteFailed = false;
413 : try {
414 2 : ifc.storeGlobalConfiguration(map1, compName);
415 0 : std::cout << " Second write succeeded (API allows duplicate composition names)\n";
416 2 : } catch (std::exception const& e) {
417 2 : secondWriteFailed = true;
418 2 : std::cout << " Second write rejected: " << e.what() << "\n";
419 2 : }
420 :
421 2 : if (secondWriteFailed) {
422 2 : std::cout << " API BEHAVIOR: Duplicate composition names are REJECTED\n";
423 : } else {
424 0 : std::cout << " API BEHAVIOR: Duplicate composition names are ALLOWED (overwrite)\n";
425 : }
426 :
427 2 : auto allComps = ifc.findAllGlobalConfigurations();
428 2 : bool found = (allComps.find(compName) != allComps.end());
429 :
430 2 : BOOST_CHECK_EQUAL(found, true);
431 :
432 2 : std::cout << " Verified: Composition exists after duplicate attempt\n";
433 2 : }
434 :
435 4 : BOOST_AUTO_TEST_CASE(different_compositions_same_config_types) {
436 2 : std::cout << "\n=== TEST 7: Different compositions can have same config types ===\n";
437 :
438 2 : auto ifc = DatabaseConfigurationInterface();
439 :
440 2 : int version1 = fixture.baseVersion + 2000;
441 :
442 2 : std::shared_ptr<ConfigurationBase> cfg1 = std::make_shared<TestConfiguration001>();
443 2 : cfg1->getView().version = version1;
444 2 : ifc.saveActiveVersion(cfg1.get());
445 :
446 6 : std::string compName1 = std::string{"MultiComp1_"} + std::to_string(fixture.baseVersion);
447 :
448 2 : auto map1 = DatabaseConfigurationInterface::config_version_map_t{};
449 2 : map1[cfg1->getConfigurationName()] = version1;
450 :
451 2 : ifc.storeGlobalConfiguration(map1, compName1);
452 2 : std::cout << "Created composition 1: " << compName1 << "\n";
453 :
454 6 : std::string compName2 = std::string{"MultiComp2_"} + std::to_string(fixture.baseVersion);
455 :
456 2 : auto map2 = DatabaseConfigurationInterface::config_version_map_t{};
457 2 : map2[cfg1->getConfigurationName()] = version1;
458 :
459 2 : bool secondSucceeded = true;
460 : try {
461 2 : ifc.storeGlobalConfiguration(map2, compName2);
462 2 : std::cout << "Created composition 2: " << compName2 << "\n";
463 0 : } catch (std::exception const& e) {
464 0 : secondSucceeded = false;
465 0 : std::cout << " Second composition failed (unexpected): " << e.what() << "\n";
466 0 : }
467 :
468 2 : BOOST_CHECK_EQUAL(secondSucceeded, true);
469 :
470 2 : auto allComps = ifc.findAllGlobalConfigurations();
471 :
472 2 : bool found1 = (allComps.find(compName1) != allComps.end());
473 2 : bool found2 = (allComps.find(compName2) != allComps.end());
474 :
475 2 : BOOST_CHECK_EQUAL(found1, true);
476 2 : BOOST_CHECK_EQUAL(found2, true);
477 :
478 2 : std::cout << " Verified: Both compositions exist independently\n";
479 2 : }
480 :
481 4 : BOOST_AUTO_TEST_CASE(sequential_multi_operation) {
482 2 : std::cout << "\n=== TEST 8: Sequential multi-operation test ===\n";
483 :
484 2 : const int NUM_OPERATIONS = 5;
485 2 : int successCount = 0;
486 2 : int failCount = 0;
487 :
488 2 : auto ifc = DatabaseConfigurationInterface();
489 :
490 2 : auto startTime = std::chrono::high_resolution_clock::now();
491 :
492 12 : for (int i = 0; i < NUM_OPERATIONS; i++) {
493 : try {
494 10 : int version = fixture.baseVersion + 3000 + (i * 10);
495 :
496 10 : std::shared_ptr<ConfigurationBase> cfg = std::make_shared<TestConfiguration001>();
497 10 : cfg->getView().version = version;
498 10 : ifc.saveActiveVersion(cfg.get());
499 :
500 10 : auto compositionMap = DatabaseConfigurationInterface::config_version_map_t{};
501 10 : compositionMap[cfg->getConfigurationName()] = version;
502 :
503 30 : std::string compName = std::string{"SeqTest_"} + std::to_string(fixture.baseVersion) + "_op" + std::to_string(i);
504 :
505 10 : ifc.storeGlobalConfiguration(compositionMap, compName);
506 :
507 10 : std::cout << " Operation " << i << ": Created composition " << compName << "\n";
508 :
509 10 : successCount++;
510 10 : } catch (std::exception const& e) {
511 0 : std::cout << " Operation " << i << " failed: " << e.what() << "\n";
512 0 : failCount++;
513 0 : }
514 : }
515 :
516 2 : auto endTime = std::chrono::high_resolution_clock::now();
517 2 : auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);
518 :
519 2 : std::cout << " Completed in " << duration.count() << "ms\n";
520 2 : std::cout << " Success: " << successCount << ", Failed: " << failCount << "\n";
521 :
522 2 : BOOST_CHECK_EQUAL(successCount, NUM_OPERATIONS);
523 :
524 2 : auto allComps = ifc.findAllGlobalConfigurations();
525 :
526 2 : int foundCount = 0;
527 12 : for (int i = 0; i < NUM_OPERATIONS; i++) {
528 30 : std::string compName = std::string{"SeqTest_"} + std::to_string(fixture.baseVersion) + "_op" + std::to_string(i);
529 10 : if (allComps.find(compName) != allComps.end()) {
530 10 : foundCount++;
531 : }
532 10 : }
533 :
534 2 : BOOST_CHECK_EQUAL(foundCount, NUM_OPERATIONS);
535 2 : std::cout << " Verified: All " << foundCount << " sequential compositions exist\n";
536 2 : }
537 :
538 4 : BOOST_AUTO_TEST_CASE(performance_store_load) {
539 2 : std::cout << "\n=== TEST 9: Performance test (store/load) ===\n";
540 :
541 2 : auto ifc = DatabaseConfigurationInterface();
542 :
543 2 : const int NUM_CONFIGS = 2;
544 2 : int baseVer = fixture.baseVersion + 4000;
545 :
546 2 : std::shared_ptr<ConfigurationBase> cfg1 = std::make_shared<TestConfiguration001>();
547 2 : cfg1->getView().version = baseVer;
548 2 : ifc.saveActiveVersion(cfg1.get());
549 :
550 2 : std::shared_ptr<ConfigurationBase> cfg2 = std::make_shared<TestConfiguration002>();
551 2 : cfg2->getView().version = baseVer + 1;
552 2 : ifc.saveActiveVersion(cfg2.get());
553 :
554 2 : auto compositionMap = DatabaseConfigurationInterface::config_version_map_t{};
555 2 : compositionMap[cfg1->getConfigurationName()] = baseVer;
556 2 : compositionMap[cfg2->getConfigurationName()] = baseVer + 1;
557 :
558 6 : std::string compName = std::string{"PerfTest_"} + std::to_string(fixture.baseVersion);
559 :
560 2 : auto storeStart = std::chrono::high_resolution_clock::now();
561 2 : ifc.storeGlobalConfiguration(compositionMap, compName);
562 2 : auto storeEnd = std::chrono::high_resolution_clock::now();
563 2 : auto storeDuration = std::chrono::duration_cast<std::chrono::milliseconds>(storeEnd - storeStart);
564 :
565 2 : std::cout << " Store time: " << storeDuration.count() << "ms\n";
566 :
567 2 : const int LOAD_ITERATIONS = 10;
568 2 : auto loadStart = std::chrono::high_resolution_clock::now();
569 22 : for (int i = 0; i < LOAD_ITERATIONS; i++) {
570 20 : auto loadedMap = ifc.loadGlobalConfiguration(compName);
571 20 : BOOST_CHECK_EQUAL(loadedMap.size(), NUM_CONFIGS);
572 20 : }
573 2 : auto loadEnd = std::chrono::high_resolution_clock::now();
574 2 : auto loadDuration = std::chrono::duration_cast<std::chrono::milliseconds>(loadEnd - loadStart);
575 :
576 2 : std::cout << " Load time (" << LOAD_ITERATIONS << " iterations): " << loadDuration.count() << "ms\n";
577 2 : std::cout << " Average load time: " << (loadDuration.count() / LOAD_ITERATIONS) << "ms\n";
578 :
579 2 : BOOST_CHECK_LT(storeDuration.count(), 5000);
580 2 : BOOST_CHECK_LT(loadDuration.count(), 5000);
581 :
582 2 : std::cout << " Verified: Performance within acceptable limits\n";
583 2 : }
584 :
585 4 : BOOST_AUTO_TEST_CASE(empty_composition_handling) {
586 2 : std::cout << "\n=== TEST 10: Empty composition handling ===\n";
587 :
588 2 : std::cout << " API BEHAVIOR: Empty compositions are NOT allowed (assertion enforced)\n";
589 2 : std::cout << " The storeGlobalConfiguration API requires at least one configuration.\n";
590 :
591 2 : auto ifc = DatabaseConfigurationInterface();
592 :
593 2 : int version = fixture.baseVersion + 4500;
594 2 : std::shared_ptr<ConfigurationBase> cfg = std::make_shared<TestConfiguration001>();
595 2 : cfg->getView().version = version;
596 2 : ifc.saveActiveVersion(cfg.get());
597 :
598 2 : auto minimalMap = DatabaseConfigurationInterface::config_version_map_t{};
599 2 : minimalMap[cfg->getConfigurationName()] = version;
600 :
601 6 : std::string compName = std::string{"MinimalCompTest_"} + std::to_string(fixture.baseVersion);
602 :
603 2 : bool storeSucceeded = false;
604 : try {
605 2 : ifc.storeGlobalConfiguration(minimalMap, compName);
606 2 : storeSucceeded = true;
607 2 : std::cout << " Minimal (single-entry) composition: succeeded\n";
608 0 : } catch (std::exception const& e) {
609 0 : std::cout << " Minimal composition failed: " << e.what() << "\n";
610 0 : }
611 :
612 2 : BOOST_CHECK_EQUAL(storeSucceeded, true);
613 2 : std::cout << " Verified: Minimal compositions are allowed\n";
614 2 : }
615 :
616 4 : BOOST_AUTO_TEST_CASE(version_boundary_values) {
617 2 : std::cout << "\n=== TEST 11: Version boundary values ===\n";
618 :
619 2 : auto ifc = DatabaseConfigurationInterface();
620 :
621 2 : std::shared_ptr<ConfigurationBase> cfg1 = std::make_shared<TestConfiguration001>();
622 2 : cfg1->getView().version = 1;
623 :
624 2 : bool v1Succeeded = true;
625 : try {
626 2 : ifc.saveActiveVersion(cfg1.get());
627 2 : std::cout << " Version 1: Save succeeded\n";
628 0 : } catch (std::exception const& e) {
629 0 : v1Succeeded = false;
630 0 : std::cout << " Version 1: Save failed - " << e.what() << "\n";
631 0 : }
632 :
633 2 : std::shared_ptr<ConfigurationBase> cfg2 = std::make_shared<TestConfiguration001>();
634 2 : cfg2->getView().version = 999999;
635 :
636 2 : bool vLargeSucceeded = true;
637 : try {
638 2 : ifc.saveActiveVersion(cfg2.get());
639 2 : std::cout << " Version 999999: Save succeeded\n";
640 0 : } catch (std::exception const& e) {
641 0 : vLargeSucceeded = false;
642 0 : std::cout << " Version 999999: Save failed - " << e.what() << "\n";
643 0 : }
644 :
645 2 : if (v1Succeeded && vLargeSucceeded) {
646 2 : auto compositionMap = DatabaseConfigurationInterface::config_version_map_t{};
647 2 : compositionMap[cfg1->getConfigurationName()] = 999999;
648 :
649 6 : std::string compName = std::string{"BoundaryTest_"} + std::to_string(fixture.baseVersion);
650 :
651 2 : bool compSucceeded = true;
652 : try {
653 2 : ifc.storeGlobalConfiguration(compositionMap, compName);
654 2 : std::cout << " Composition with large version: succeeded\n";
655 0 : } catch (std::exception const& e) {
656 0 : compSucceeded = false;
657 0 : std::cout << " Composition with large version: failed - " << e.what() << "\n";
658 0 : }
659 :
660 2 : BOOST_CHECK_EQUAL(compSucceeded, true);
661 2 : }
662 :
663 2 : BOOST_CHECK_EQUAL(v1Succeeded, true);
664 :
665 2 : std::cout << " Verified: Boundary version values handled\n";
666 2 : }
667 :
668 4 : BOOST_AUTO_TEST_CASE(index_consistency_validation) {
669 2 : std::cout << "\n=== TEST 12: Index consistency validation ===\n";
670 :
671 2 : if (!isFileSystemBackend()) {
672 1 : std::cout << " Skipping: Not using FileSystemDB backend\n";
673 1 : BOOST_CHECK(true);
674 1 : return;
675 : }
676 :
677 1 : auto ifc = DatabaseConfigurationInterface();
678 :
679 1 : int version = fixture.baseVersion + 5000;
680 :
681 1 : std::shared_ptr<ConfigurationBase> cfg = std::make_shared<TestConfiguration001>();
682 1 : cfg->getView().version = version;
683 1 : ifc.saveActiveVersion(cfg.get());
684 :
685 1 : auto compositionMap = DatabaseConfigurationInterface::config_version_map_t{};
686 1 : compositionMap[cfg->getConfigurationName()] = version;
687 :
688 3 : std::string compName = std::string{"IndexTest_"} + std::to_string(fixture.baseVersion);
689 1 : ifc.storeGlobalConfiguration(compositionMap, compName);
690 :
691 1 : std::string globalConfigDir = fixture.dbPath + "/GlobalConfigurations";
692 1 : std::string indexPath = globalConfigDir + "/index.json";
693 :
694 1 : std::cout << " Checking index: " << indexPath << "\n";
695 :
696 1 : auto indexOids = extractOidsFromIndex(indexPath);
697 1 : std::cout << " OIDs in index: " << indexOids.size() << "\n";
698 :
699 1 : auto jsonFiles = listJsonFiles(globalConfigDir);
700 1 : std::set<std::string> fileOids;
701 1 : for (const auto& filepath : jsonFiles) {
702 0 : std::string oid = getOidFromFilename(filepath);
703 0 : if (!oid.empty() && oid != "index") {
704 0 : fileOids.insert(oid);
705 : }
706 0 : }
707 1 : std::cout << " Files on disk: " << fileOids.size() << "\n";
708 :
709 1 : int orphanedFiles = 0;
710 1 : for (const auto& fileOid : fileOids) {
711 0 : if (indexOids.find(fileOid) == indexOids.end()) {
712 0 : std::cout << " WARNING: Orphaned file: " << fileOid << ".json\n";
713 0 : orphanedFiles++;
714 : }
715 : }
716 :
717 1 : int missingFiles = 0;
718 1 : for (const auto& indexOid : indexOids) {
719 0 : if (fileOids.find(indexOid) == fileOids.end()) {
720 0 : std::cout << " WARNING: Missing file: " << indexOid << ".json\n";
721 0 : missingFiles++;
722 : }
723 : }
724 :
725 1 : std::cout << " Orphaned files: " << orphanedFiles << "\n";
726 1 : std::cout << " Missing files: " << missingFiles << "\n";
727 :
728 1 : BOOST_WARN_EQUAL(orphanedFiles, 0);
729 1 : BOOST_WARN_EQUAL(missingFiles, 0);
730 :
731 1 : BOOST_CHECK_EQUAL(missingFiles, 0);
732 :
733 1 : std::cout << " Verified: Index consistency checked\n";
734 1 : }
735 :
736 4 : BOOST_AUTO_TEST_CASE(stress_test_rapid_operations) {
737 2 : std::cout << "\n=== TEST 13: Stress test (rapid operations) ===\n";
738 :
739 2 : auto ifc = DatabaseConfigurationInterface();
740 :
741 2 : const int NUM_OPERATIONS = 20;
742 2 : int successCount = 0;
743 2 : int failCount = 0;
744 :
745 2 : auto startTime = std::chrono::high_resolution_clock::now();
746 :
747 42 : for (int i = 0; i < NUM_OPERATIONS; i++) {
748 : try {
749 40 : int version = fixture.baseVersion + 6000 + i;
750 :
751 40 : std::shared_ptr<ConfigurationBase> cfg = std::make_shared<TestConfiguration001>();
752 40 : cfg->getView().version = version;
753 40 : ifc.saveActiveVersion(cfg.get());
754 :
755 40 : auto compositionMap = DatabaseConfigurationInterface::config_version_map_t{};
756 40 : compositionMap[cfg->getConfigurationName()] = version;
757 :
758 120 : std::string compName = std::string{"StressTest_"} + std::to_string(fixture.baseVersion) + "_" + std::to_string(i);
759 40 : ifc.storeGlobalConfiguration(compositionMap, compName);
760 :
761 40 : successCount++;
762 40 : } catch (std::exception const& e) {
763 0 : std::cout << " Operation " << i << " failed: " << e.what() << "\n";
764 0 : failCount++;
765 0 : }
766 : }
767 :
768 2 : auto endTime = std::chrono::high_resolution_clock::now();
769 2 : auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);
770 :
771 2 : std::cout << " Completed " << NUM_OPERATIONS << " operations in " << duration.count() << "ms\n";
772 2 : std::cout << " Average: " << (duration.count() / NUM_OPERATIONS) << "ms per operation\n";
773 2 : std::cout << " Success: " << successCount << ", Failed: " << failCount << "\n";
774 :
775 2 : BOOST_CHECK_EQUAL(successCount, NUM_OPERATIONS);
776 2 : BOOST_CHECK_EQUAL(failCount, 0);
777 :
778 2 : BOOST_CHECK_LT(duration.count(), 30000);
779 :
780 2 : if (isFileSystemBackend()) {
781 1 : std::string globalConfigDir = fixture.dbPath + "/GlobalConfigurations";
782 1 : auto jsonFiles = listJsonFiles(globalConfigDir);
783 :
784 1 : int totalDuplicates = 0;
785 1 : for (const auto& filepath : jsonFiles) {
786 0 : std::string content = readFileContent(filepath);
787 0 : if (content.empty()) continue;
788 :
789 0 : auto configNames = extractConfigNames(content);
790 0 : std::map<std::string, int> nameCounts;
791 0 : for (const auto& name : configNames) {
792 0 : nameCounts[name]++;
793 : }
794 :
795 0 : for (const auto& entry : nameCounts) {
796 0 : if (entry.second > 1) {
797 0 : totalDuplicates++;
798 : }
799 : }
800 0 : }
801 :
802 1 : BOOST_CHECK_EQUAL(totalDuplicates, 0);
803 1 : std::cout << " Verified: No duplicates in storage after stress test\n";
804 1 : }
805 2 : }
806 :
807 4 : BOOST_AUTO_TEST_CASE(composition_name_edge_cases) {
808 2 : std::cout << "\n=== TEST 14: Composition name edge cases ===\n";
809 :
810 2 : auto ifc = DatabaseConfigurationInterface();
811 :
812 2 : int version = fixture.baseVersion + 7000;
813 :
814 2 : std::shared_ptr<ConfigurationBase> cfg = std::make_shared<TestConfiguration001>();
815 2 : cfg->getView().version = version;
816 2 : ifc.saveActiveVersion(cfg.get());
817 :
818 2 : auto compositionMap = DatabaseConfigurationInterface::config_version_map_t{};
819 2 : compositionMap[cfg->getConfigurationName()] = version;
820 :
821 6 : std::string name1 = std::string{"Test___Multiple___Underscores_"} + std::to_string(fixture.baseVersion);
822 2 : bool name1Succeeded = false;
823 : try {
824 2 : ifc.storeGlobalConfiguration(compositionMap, name1);
825 2 : name1Succeeded = true;
826 2 : std::cout << " Underscore-heavy name: succeeded\n";
827 0 : } catch (std::exception const& e) {
828 0 : std::cout << " Underscore-heavy name: failed - " << e.what() << "\n";
829 0 : }
830 :
831 2 : std::string name2 = std::to_string(fixture.baseVersion) + "12345";
832 2 : bool name2Succeeded = false;
833 : try {
834 2 : cfg->getView().version = version + 1;
835 2 : ifc.saveActiveVersion(cfg.get());
836 2 : compositionMap[cfg->getConfigurationName()] = version + 1;
837 :
838 2 : ifc.storeGlobalConfiguration(compositionMap, name2);
839 2 : name2Succeeded = true;
840 2 : std::cout << " Numeric name: succeeded\n";
841 0 : } catch (std::exception const& e) {
842 0 : std::cout << " Numeric name: failed - " << e.what() << "\n";
843 0 : }
844 :
845 6 : std::string name3 = std::string{"VeryLongCompositionNameThatMightCauseIssues_"} + std::to_string(fixture.baseVersion);
846 2 : bool name3Succeeded = false;
847 : try {
848 2 : cfg->getView().version = version + 2;
849 2 : ifc.saveActiveVersion(cfg.get());
850 2 : compositionMap[cfg->getConfigurationName()] = version + 2;
851 :
852 2 : ifc.storeGlobalConfiguration(compositionMap, name3);
853 2 : name3Succeeded = true;
854 2 : std::cout << " Long name: succeeded\n";
855 0 : } catch (std::exception const& e) {
856 0 : std::cout << " Long name: failed - " << e.what() << "\n";
857 0 : }
858 :
859 2 : BOOST_CHECK_EQUAL(name1Succeeded, true);
860 2 : BOOST_CHECK_MESSAGE(true, (name2Succeeded ? "Numeric names allowed" : "Numeric names rejected"));
861 2 : BOOST_CHECK_MESSAGE(true, (name3Succeeded ? "Long names allowed" : "Long names rejected"));
862 :
863 2 : std::cout << " Verified: Composition name edge cases tested\n";
864 2 : }
865 :
866 4 : BOOST_AUTO_TEST_CASE(overwrite_updates_version) {
867 2 : std::cout << "\n=== TEST 15: Overwrite updates version (unassigns old) ===\n";
868 :
869 2 : auto ifc = DatabaseConfigurationInterface();
870 :
871 2 : int versionOld = fixture.baseVersion + 8000;
872 2 : int versionNew = fixture.baseVersion + 8001;
873 :
874 2 : std::shared_ptr<ConfigurationBase> cfgOld = std::make_shared<TestConfiguration001>();
875 2 : cfgOld->getView().version = versionOld;
876 2 : ifc.saveActiveVersion(cfgOld.get());
877 2 : std::cout << " Created old version: " << versionOld << "\n";
878 :
879 2 : std::shared_ptr<ConfigurationBase> cfgNew = std::make_shared<TestConfiguration001>();
880 2 : cfgNew->getView().version = versionNew;
881 2 : ifc.saveActiveVersion(cfgNew.get());
882 2 : std::cout << " Created new version: " << versionNew << "\n";
883 :
884 6 : std::string compName = std::string{"OverwriteVersionTest_"} + std::to_string(fixture.baseVersion);
885 :
886 2 : auto mapOld = DatabaseConfigurationInterface::config_version_map_t{};
887 2 : mapOld[cfgOld->getConfigurationName()] = versionOld;
888 :
889 2 : ifc.storeGlobalConfiguration(mapOld, compName);
890 2 : std::cout << " Created composition with version " << versionOld << "\n";
891 :
892 2 : auto loaded1 = ifc.loadGlobalConfiguration(compName);
893 2 : BOOST_CHECK_EQUAL(loaded1.size(), 1);
894 2 : BOOST_CHECK_EQUAL(loaded1[cfgOld->getConfigurationName()], versionOld);
895 2 : std::cout << " Verified: Initial composition has version " << versionOld << "\n";
896 :
897 2 : auto mapNew = DatabaseConfigurationInterface::config_version_map_t{};
898 2 : mapNew[cfgNew->getConfigurationName()] = versionNew;
899 :
900 2 : ifc.storeGlobalConfiguration(mapNew, compName, true);
901 2 : std::cout << " Overwrote composition with version " << versionNew << "\n";
902 :
903 2 : auto loaded2 = ifc.loadGlobalConfiguration(compName);
904 2 : BOOST_CHECK_EQUAL(loaded2.size(), 1);
905 2 : BOOST_CHECK_EQUAL(loaded2[cfgNew->getConfigurationName()], versionNew);
906 2 : std::cout << " Verified: Composition now has version " << versionNew << "\n";
907 :
908 2 : auto compsWithOld = ifc.findCompositionsContaining(cfgOld->getConfigurationName(), std::to_string(versionOld));
909 2 : bool oldInComp = (compsWithOld.find(compName) != compsWithOld.end());
910 2 : BOOST_CHECK_EQUAL(oldInComp, false);
911 2 : std::cout << " Verified: Old version " << versionOld << " is NOT in composition (correctly unassigned)\n";
912 :
913 2 : auto compsWithNew = ifc.findCompositionsContaining(cfgNew->getConfigurationName(), std::to_string(versionNew));
914 2 : bool newInComp = (compsWithNew.find(compName) != compsWithNew.end());
915 2 : BOOST_CHECK_EQUAL(newInComp, true);
916 2 : std::cout << " Verified: New version " << versionNew << " IS in composition\n";
917 :
918 2 : std::cout << " TEST PASSED: Overwrite correctly unassigned old version\n";
919 2 : }
920 :
921 4 : BOOST_AUTO_TEST_CASE(overwrite_removes_members) {
922 2 : std::cout << "\n=== TEST 16: Overwrite removes members ===\n";
923 :
924 2 : auto ifc = DatabaseConfigurationInterface();
925 :
926 2 : int version1 = fixture.baseVersion + 8100;
927 2 : int version2 = fixture.baseVersion + 8101;
928 2 : int version3 = fixture.baseVersion + 8102;
929 :
930 2 : std::shared_ptr<ConfigurationBase> cfg1 = std::make_shared<TestConfiguration001>();
931 2 : cfg1->getView().version = version1;
932 2 : ifc.saveActiveVersion(cfg1.get());
933 :
934 2 : std::shared_ptr<ConfigurationBase> cfg2 = std::make_shared<TestConfiguration002>();
935 2 : cfg2->getView().version = version2;
936 2 : ifc.saveActiveVersion(cfg2.get());
937 :
938 2 : std::shared_ptr<ConfigurationBase> cfg3 = std::make_shared<TestConfiguration003>();
939 2 : cfg3->getView().version = version3;
940 2 : ifc.saveActiveVersion(cfg3.get());
941 :
942 2 : std::cout << " Created 3 config versions: " << version1 << ", " << version2 << ", " << version3 << "\n";
943 :
944 6 : std::string compName = std::string{"OverwriteRemoveTest_"} + std::to_string(fixture.baseVersion);
945 :
946 2 : auto mapAll = DatabaseConfigurationInterface::config_version_map_t{};
947 2 : mapAll[cfg1->getConfigurationName()] = version1;
948 2 : mapAll[cfg2->getConfigurationName()] = version2;
949 2 : mapAll[cfg3->getConfigurationName()] = version3;
950 :
951 2 : ifc.storeGlobalConfiguration(mapAll, compName);
952 2 : std::cout << " Created composition with 3 members\n";
953 :
954 2 : auto loaded1 = ifc.loadGlobalConfiguration(compName);
955 2 : BOOST_CHECK_EQUAL(loaded1.size(), 3);
956 2 : std::cout << " Verified: Initial composition has 3 members\n";
957 :
958 2 : auto mapOne = DatabaseConfigurationInterface::config_version_map_t{};
959 2 : mapOne[cfg1->getConfigurationName()] = version1;
960 :
961 2 : ifc.storeGlobalConfiguration(mapOne, compName, true);
962 2 : std::cout << " Overwrote composition with only 1 member\n";
963 :
964 2 : auto loaded2 = ifc.loadGlobalConfiguration(compName);
965 2 : BOOST_CHECK_EQUAL(loaded2.size(), 1);
966 2 : BOOST_CHECK_EQUAL(loaded2[cfg1->getConfigurationName()], version1);
967 2 : std::cout << " Verified: Composition now has 1 member\n";
968 :
969 2 : auto compsWithCfg2 = ifc.findCompositionsContaining(cfg2->getConfigurationName(), std::to_string(version2));
970 2 : bool cfg2InComp = (compsWithCfg2.find(compName) != compsWithCfg2.end());
971 2 : BOOST_CHECK_EQUAL(cfg2InComp, false);
972 2 : std::cout << " Verified: " << cfg2->getConfigurationName() << " is NOT in composition\n";
973 :
974 2 : auto compsWithCfg3 = ifc.findCompositionsContaining(cfg3->getConfigurationName(), std::to_string(version3));
975 2 : bool cfg3InComp = (compsWithCfg3.find(compName) != compsWithCfg3.end());
976 2 : BOOST_CHECK_EQUAL(cfg3InComp, false);
977 2 : std::cout << " Verified: " << cfg3->getConfigurationName() << " is NOT in composition\n";
978 :
979 2 : auto compsWithCfg1 = ifc.findCompositionsContaining(cfg1->getConfigurationName(), std::to_string(version1));
980 2 : bool cfg1InComp = (compsWithCfg1.find(compName) != compsWithCfg1.end());
981 2 : BOOST_CHECK_EQUAL(cfg1InComp, true);
982 2 : std::cout << " Verified: " << cfg1->getConfigurationName() << " IS still in composition\n";
983 :
984 2 : std::cout << " TEST PASSED: Overwrite correctly removed members\n";
985 2 : }
986 :
987 4 : BOOST_AUTO_TEST_CASE(overwrite_with_partial_changes) {
988 2 : std::cout << "\n=== TEST 17: Overwrite with partial changes (full merge) ===\n";
989 :
990 2 : auto ifc = DatabaseConfigurationInterface();
991 :
992 2 : int vA1 = fixture.baseVersion + 8200;
993 2 : int vA5 = fixture.baseVersion + 8205;
994 2 : int vB2 = fixture.baseVersion + 8202;
995 2 : int vC3 = fixture.baseVersion + 8203;
996 :
997 2 : std::shared_ptr<ConfigurationBase> cfgA1 = std::make_shared<TestConfiguration001>();
998 2 : cfgA1->getView().version = vA1;
999 2 : ifc.saveActiveVersion(cfgA1.get());
1000 :
1001 2 : std::shared_ptr<ConfigurationBase> cfgA5 = std::make_shared<TestConfiguration001>();
1002 2 : cfgA5->getView().version = vA5;
1003 2 : ifc.saveActiveVersion(cfgA5.get());
1004 :
1005 2 : std::shared_ptr<ConfigurationBase> cfgB2 = std::make_shared<TestConfiguration002>();
1006 2 : cfgB2->getView().version = vB2;
1007 2 : ifc.saveActiveVersion(cfgB2.get());
1008 :
1009 2 : std::shared_ptr<ConfigurationBase> cfgC3 = std::make_shared<TestConfiguration003>();
1010 2 : cfgC3->getView().version = vC3;
1011 2 : ifc.saveActiveVersion(cfgC3.get());
1012 :
1013 2 : std::cout << " Created versions: A/v" << vA1 << ", A/v" << vA5 << ", B/v" << vB2 << ", C/v" << vC3 << "\n";
1014 :
1015 6 : std::string compName = std::string{"PartialChangeTest_"} + std::to_string(fixture.baseVersion);
1016 :
1017 2 : auto mapOld = DatabaseConfigurationInterface::config_version_map_t{};
1018 2 : mapOld[cfgA1->getConfigurationName()] = vA1;
1019 2 : mapOld[cfgB2->getConfigurationName()] = vB2;
1020 2 : mapOld[cfgC3->getConfigurationName()] = vC3;
1021 :
1022 2 : ifc.storeGlobalConfiguration(mapOld, compName);
1023 2 : std::cout << " Created OLD composition: {A/v" << vA1 << ", B/v" << vB2 << ", C/v" << vC3 << "}\n";
1024 :
1025 2 : auto loaded1 = ifc.loadGlobalConfiguration(compName);
1026 2 : BOOST_CHECK_EQUAL(loaded1.size(), 3);
1027 :
1028 2 : auto mapNew = DatabaseConfigurationInterface::config_version_map_t{};
1029 2 : mapNew[cfgA5->getConfigurationName()] = vA5;
1030 2 : mapNew[cfgB2->getConfigurationName()] = vB2;
1031 :
1032 2 : ifc.storeGlobalConfiguration(mapNew, compName, true);
1033 2 : std::cout << " Overwrote with NEW composition: {A/v" << vA5 << ", B/v" << vB2 << "}\n";
1034 :
1035 2 : auto loaded2 = ifc.loadGlobalConfiguration(compName);
1036 2 : BOOST_CHECK_EQUAL(loaded2.size(), 2);
1037 2 : BOOST_CHECK_EQUAL(loaded2[cfgA5->getConfigurationName()], vA5);
1038 2 : BOOST_CHECK_EQUAL(loaded2[cfgB2->getConfigurationName()], vB2);
1039 2 : std::cout << " Verified: Composition has 2 members with correct versions\n";
1040 :
1041 2 : auto compsWithA1 = ifc.findCompositionsContaining(cfgA1->getConfigurationName(), std::to_string(vA1));
1042 2 : bool A1InComp = (compsWithA1.find(compName) != compsWithA1.end());
1043 2 : BOOST_CHECK_EQUAL(A1InComp, false);
1044 2 : std::cout << " Verified: A/v" << vA1 << " is NOT in composition (replaced)\n";
1045 :
1046 2 : auto compsWithA5 = ifc.findCompositionsContaining(cfgA5->getConfigurationName(), std::to_string(vA5));
1047 2 : bool A5InComp = (compsWithA5.find(compName) != compsWithA5.end());
1048 2 : BOOST_CHECK_EQUAL(A5InComp, true);
1049 2 : std::cout << " Verified: A/v" << vA5 << " IS in composition (new version)\n";
1050 :
1051 2 : auto compsWithB2 = ifc.findCompositionsContaining(cfgB2->getConfigurationName(), std::to_string(vB2));
1052 2 : bool B2InComp = (compsWithB2.find(compName) != compsWithB2.end());
1053 2 : BOOST_CHECK_EQUAL(B2InComp, true);
1054 2 : std::cout << " Verified: B/v" << vB2 << " IS in composition (unchanged)\n";
1055 :
1056 2 : auto compsWithC3 = ifc.findCompositionsContaining(cfgC3->getConfigurationName(), std::to_string(vC3));
1057 2 : bool C3InComp = (compsWithC3.find(compName) != compsWithC3.end());
1058 2 : BOOST_CHECK_EQUAL(C3InComp, false);
1059 2 : std::cout << " Verified: C/v" << vC3 << " is NOT in composition (removed)\n";
1060 :
1061 2 : std::cout << " TEST PASSED: Partial changes correctly applied\n";
1062 2 : }
1063 :
1064 4 : BOOST_AUTO_TEST_CASE(load_after_overwrite_returns_only_new_members) {
1065 2 : std::cout << "\n=== TEST 18: Load after overwrite returns only new members ===\n";
1066 :
1067 2 : auto ifc = DatabaseConfigurationInterface();
1068 :
1069 2 : int v1 = fixture.baseVersion + 8300;
1070 2 : int v2 = fixture.baseVersion + 8301;
1071 2 : int v3 = fixture.baseVersion + 8302;
1072 2 : int v4 = fixture.baseVersion + 8303;
1073 :
1074 2 : std::shared_ptr<ConfigurationBase> cfg1a = std::make_shared<TestConfiguration001>();
1075 2 : cfg1a->getView().version = v1;
1076 2 : ifc.saveActiveVersion(cfg1a.get());
1077 :
1078 2 : std::shared_ptr<ConfigurationBase> cfg1b = std::make_shared<TestConfiguration001>();
1079 2 : cfg1b->getView().version = v2;
1080 2 : ifc.saveActiveVersion(cfg1b.get());
1081 :
1082 2 : std::shared_ptr<ConfigurationBase> cfg2a = std::make_shared<TestConfiguration002>();
1083 2 : cfg2a->getView().version = v3;
1084 2 : ifc.saveActiveVersion(cfg2a.get());
1085 :
1086 2 : std::shared_ptr<ConfigurationBase> cfg2b = std::make_shared<TestConfiguration002>();
1087 2 : cfg2b->getView().version = v4;
1088 2 : ifc.saveActiveVersion(cfg2b.get());
1089 :
1090 6 : std::string compName = std::string{"LoadAfterOverwriteTest_"} + std::to_string(fixture.baseVersion);
1091 :
1092 2 : auto map1 = DatabaseConfigurationInterface::config_version_map_t{};
1093 2 : map1[cfg1a->getConfigurationName()] = v1;
1094 2 : map1[cfg2a->getConfigurationName()] = v3;
1095 :
1096 2 : ifc.storeGlobalConfiguration(map1, compName);
1097 2 : std::cout << " Created initial: {Type1/v" << v1 << ", Type2/v" << v3 << "}\n";
1098 :
1099 2 : auto map2 = DatabaseConfigurationInterface::config_version_map_t{};
1100 2 : map2[cfg1b->getConfigurationName()] = v2;
1101 2 : map2[cfg2b->getConfigurationName()] = v4;
1102 :
1103 2 : ifc.storeGlobalConfiguration(map2, compName, true);
1104 2 : std::cout << " Overwrote with: {Type1/v" << v2 << ", Type2/v" << v4 << "}\n";
1105 :
1106 2 : auto loaded = ifc.loadGlobalConfiguration(compName);
1107 :
1108 2 : BOOST_CHECK_EQUAL(loaded.size(), 2);
1109 2 : BOOST_CHECK_EQUAL(loaded[cfg1b->getConfigurationName()], v2);
1110 2 : BOOST_CHECK_EQUAL(loaded[cfg2b->getConfigurationName()], v4);
1111 :
1112 2 : BOOST_CHECK(loaded[cfg1a->getConfigurationName()] != v1);
1113 2 : BOOST_CHECK(loaded[cfg2a->getConfigurationName()] != v3);
1114 :
1115 2 : std::cout << " Verified: Load returns exactly {Type1/v" << v2 << ", Type2/v" << v4 << "}\n";
1116 2 : std::cout << " TEST PASSED: Load after overwrite returns only new members\n";
1117 2 : }
1118 :
1119 4 : BOOST_AUTO_TEST_CASE(reverse_lookup_correct_after_overwrite) {
1120 2 : std::cout << "\n=== TEST 19: Reverse lookup correct after overwrite ===\n";
1121 :
1122 2 : auto ifc = DatabaseConfigurationInterface();
1123 :
1124 2 : int vOld = fixture.baseVersion + 8400;
1125 2 : int vNew = fixture.baseVersion + 8401;
1126 :
1127 2 : std::shared_ptr<ConfigurationBase> cfgOld = std::make_shared<TestConfiguration001>();
1128 2 : cfgOld->getView().version = vOld;
1129 2 : ifc.saveActiveVersion(cfgOld.get());
1130 :
1131 2 : std::shared_ptr<ConfigurationBase> cfgNew = std::make_shared<TestConfiguration001>();
1132 2 : cfgNew->getView().version = vNew;
1133 2 : ifc.saveActiveVersion(cfgNew.get());
1134 :
1135 6 : std::string compName = std::string{"ReverseLookupOverwriteTest_"} + std::to_string(fixture.baseVersion);
1136 :
1137 2 : auto mapOld = DatabaseConfigurationInterface::config_version_map_t{};
1138 2 : mapOld[cfgOld->getConfigurationName()] = vOld;
1139 2 : ifc.storeGlobalConfiguration(mapOld, compName);
1140 2 : std::cout << " Created composition with version " << vOld << "\n";
1141 :
1142 2 : auto comps1 = ifc.findCompositionsContaining(cfgOld->getConfigurationName(), std::to_string(vOld));
1143 2 : BOOST_CHECK(comps1.find(compName) != comps1.end());
1144 2 : std::cout << " Verified: Reverse lookup finds old version\n";
1145 :
1146 2 : auto mapNew = DatabaseConfigurationInterface::config_version_map_t{};
1147 2 : mapNew[cfgNew->getConfigurationName()] = vNew;
1148 2 : ifc.storeGlobalConfiguration(mapNew, compName, true);
1149 2 : std::cout << " Overwrote composition with version " << vNew << "\n";
1150 :
1151 2 : auto comps2 = ifc.findCompositionsContaining(cfgOld->getConfigurationName(), std::to_string(vOld));
1152 2 : bool oldFound = (comps2.find(compName) != comps2.end());
1153 2 : BOOST_CHECK_EQUAL(oldFound, false);
1154 2 : std::cout << " Verified: Reverse lookup for old version returns EMPTY (correctly unassigned)\n";
1155 :
1156 2 : auto comps3 = ifc.findCompositionsContaining(cfgNew->getConfigurationName(), std::to_string(vNew));
1157 2 : bool newFound = (comps3.find(compName) != comps3.end());
1158 2 : BOOST_CHECK_EQUAL(newFound, true);
1159 2 : std::cout << " Verified: Reverse lookup for new version finds it\n";
1160 :
1161 2 : std::cout << " TEST PASSED: Reverse lookup is correct after overwrite\n";
1162 2 : }
1163 :
1164 4 : BOOST_AUTO_TEST_CASE(overwrite_prevents_too_many_results) {
1165 2 : std::cout << "\n=== TEST 20: Overwrite prevents 'too many results' error ===\n";
1166 :
1167 2 : auto ifc = DatabaseConfigurationInterface();
1168 :
1169 6 : std::string compName = std::string{"TooManyResultsTest_"} + std::to_string(fixture.baseVersion);
1170 :
1171 2 : const int NUM_OVERWRITES = 5;
1172 2 : int baseVer = fixture.baseVersion + 8500;
1173 :
1174 2 : std::cout << " Performing " << NUM_OVERWRITES << " sequential overwrites...\n";
1175 :
1176 12 : for (int i = 0; i < NUM_OVERWRITES; i++) {
1177 10 : int version = baseVer + i;
1178 :
1179 10 : std::shared_ptr<ConfigurationBase> cfg = std::make_shared<TestConfiguration001>();
1180 10 : cfg->getView().version = version;
1181 10 : ifc.saveActiveVersion(cfg.get());
1182 :
1183 10 : auto compositionMap = DatabaseConfigurationInterface::config_version_map_t{};
1184 10 : compositionMap[cfg->getConfigurationName()] = version;
1185 :
1186 10 : bool allowOverwrite = (i > 0);
1187 10 : ifc.storeGlobalConfiguration(compositionMap, compName, allowOverwrite);
1188 :
1189 10 : std::cout << " Overwrite " << i << ": Set to version " << version << "\n";
1190 10 : }
1191 :
1192 2 : bool loadSucceeded = false;
1193 2 : int loadedVersion = 0;
1194 2 : std::string errorMsg;
1195 :
1196 : try {
1197 2 : auto loaded = ifc.loadGlobalConfiguration(compName);
1198 2 : loadSucceeded = true;
1199 :
1200 2 : BOOST_CHECK_EQUAL(loaded.size(), 1);
1201 :
1202 2 : int expectedVersion = baseVer + NUM_OVERWRITES - 1;
1203 2 : loadedVersion = loaded.begin()->second;
1204 2 : BOOST_CHECK_EQUAL(loadedVersion, expectedVersion);
1205 :
1206 2 : std::cout << " Load succeeded! Got version " << loadedVersion << "\n";
1207 2 : } catch (std::exception const& e) {
1208 0 : errorMsg = e.what();
1209 0 : std::cout << " Load FAILED: " << errorMsg << "\n";
1210 0 : }
1211 :
1212 2 : BOOST_CHECK_MESSAGE(loadSucceeded,
1213 : "loadGlobalConfiguration should NOT fail after multiple overwrites. "
1214 : "Error: " +
1215 : errorMsg);
1216 :
1217 2 : if (loadSucceeded) {
1218 2 : std::cout << " Verified: No 'too many results' error after " << NUM_OVERWRITES << " overwrites\n";
1219 2 : std::cout << " Verified: Final version is " << loadedVersion << " (expected " << (baseVer + NUM_OVERWRITES - 1) << ")\n";
1220 : }
1221 :
1222 2 : std::cout << " TEST PASSED: Overwrite correctly prevents 'too many results' error\n";
1223 2 : }
1224 :
1225 4 : BOOST_AUTO_TEST_CASE(overwrite_with_identical_composition) {
1226 2 : std::cout << "\n=== TEST 21: Overwrite with identical composition (no-op) ===\n";
1227 :
1228 2 : auto ifc = DatabaseConfigurationInterface();
1229 :
1230 2 : int version1 = fixture.baseVersion + 8600;
1231 2 : int version2 = fixture.baseVersion + 8601;
1232 :
1233 2 : std::shared_ptr<ConfigurationBase> cfg1 = std::make_shared<TestConfiguration001>();
1234 2 : cfg1->getView().version = version1;
1235 2 : ifc.saveActiveVersion(cfg1.get());
1236 :
1237 2 : std::shared_ptr<ConfigurationBase> cfg2 = std::make_shared<TestConfiguration002>();
1238 2 : cfg2->getView().version = version2;
1239 2 : ifc.saveActiveVersion(cfg2.get());
1240 :
1241 6 : std::string compName = std::string{"IdenticalOverwriteTest_"} + std::to_string(fixture.baseVersion);
1242 :
1243 2 : auto compositionMap = DatabaseConfigurationInterface::config_version_map_t{};
1244 2 : compositionMap[cfg1->getConfigurationName()] = version1;
1245 2 : compositionMap[cfg2->getConfigurationName()] = version2;
1246 :
1247 2 : ifc.storeGlobalConfiguration(compositionMap, compName);
1248 2 : std::cout << " Created initial composition\n";
1249 :
1250 2 : bool overwriteSucceeded = false;
1251 : try {
1252 2 : ifc.storeGlobalConfiguration(compositionMap, compName, true);
1253 2 : overwriteSucceeded = true;
1254 2 : std::cout << " Overwrote with identical composition: succeeded\n";
1255 0 : } catch (std::exception const& e) {
1256 0 : std::cout << " Overwrote with identical composition: FAILED - " << e.what() << "\n";
1257 0 : }
1258 :
1259 2 : BOOST_CHECK_EQUAL(overwriteSucceeded, true);
1260 :
1261 2 : auto loaded = ifc.loadGlobalConfiguration(compName);
1262 2 : BOOST_CHECK_EQUAL(loaded.size(), 2);
1263 2 : BOOST_CHECK_EQUAL(loaded[cfg1->getConfigurationName()], version1);
1264 2 : BOOST_CHECK_EQUAL(loaded[cfg2->getConfigurationName()], version2);
1265 :
1266 2 : std::cout << " Verified: Composition unchanged after identical overwrite\n";
1267 2 : std::cout << " TEST PASSED: Identical overwrite is a successful no-op\n";
1268 2 : }
1269 :
1270 : BOOST_AUTO_TEST_SUITE_END()
|