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()
|