@@ -312,3 +312,203 @@ RecordFile::BatchAppendContext::~BatchAppendContext()
312
312
if (!errStr.isEmpty ())
313
313
Fatal () << errStr; // app will quit in main event loop after printing error.
314
314
}
315
+
316
+ #ifdef ENABLE_TESTS
317
+ #include " App.h"
318
+ #include < QTemporaryFile>
319
+ #include < algorithm>
320
+ #include < atomic>
321
+ #include < cstddef>
322
+ #include < cstring>
323
+ #include < thread>
324
+ #include < type_traits>
325
+ #include < vector>
326
+
327
+ namespace {
328
+ void testRecordFile () {
329
+ size_t nChecksOK = 0 ;
330
+
331
+ const auto fileName = []{
332
+ QTemporaryFile tmp (APPNAME " _XXXXXX.tmp" );
333
+ tmp.open ();
334
+ auto ret = tmp.fileName ();
335
+ tmp.setAutoRemove (false ); // keep file around so we can pass it to RecordFile instance
336
+ return ret;
337
+ }();
338
+ constexpr size_t HashLen = 32 ;
339
+ constexpr size_t N = 100'000 ; // 100k items
340
+ Log () << " Testing Recordfile \" " << fileName << " \" with " << N << " " << HashLen << " -byte random records ..." ;
341
+ // delete tmp file at scope end
342
+ Defer d ([&fileName] {
343
+ QFile::remove (fileName);
344
+ Log () << " Temporary file: \" " << fileName << " \" deleted" ;
345
+ });
346
+
347
+ Tic t0;
348
+ std::vector<QByteArray> hashes (N, QByteArray (HashLen, Qt::Uninitialized));
349
+ // randomize hashes
350
+ QByteArray *lastH = nullptr ;
351
+ for (auto & h : hashes) {
352
+ Util::getRandomBytes (h.data (), h.size ());
353
+ if (lastH && *lastH == h)
354
+ throw Exception (" Something went wrong generating random hashes.. previous hash and this hash match!" );
355
+ lastH = &h;
356
+ }
357
+ Log () << " Generated " << hashes.size () << " random hahses in " << t0.msecStr () << " msec" ;
358
+ {
359
+ t0 = Tic ();
360
+ RecordFile f (fileName, HashLen);
361
+ auto batch = f.beginBatchAppend ();
362
+ QString err;
363
+ for (const auto & h : hashes)
364
+ if (!batch.append (h, &err))
365
+ throw Exception (QString (" Failed to append a record using batch append to RecordFile: %1" ).arg (err));
366
+ Log () << " Wrote " << f.numRecords () << " hahses in " << t0.msecStr () << " msec" ;
367
+ }
368
+ {
369
+ t0 = Tic ();
370
+ // Read 1 record at a time randomly from 3 threads concurrently
371
+ std::vector<std::thread> thrds;
372
+ RecordFile f (fileName, HashLen);
373
+ if (f.numRecords () != N) throw Exception (" RecordFile has wrong number of records!" );
374
+ std::atomic_size_t ctr{0 };
375
+ QString fail_shared;
376
+ std::mutex fail_shared_mut;
377
+ for (size_t i = 0 ; i < 3 ; ++i) {
378
+ thrds.emplace_back ([&]{
379
+ QString fail;
380
+ for (size_t i = 0 ; fail.isEmpty () && i < hashes.size () * 7 / 20 ; ++i) {
381
+ size_t idx;
382
+ Util::getRandomBytes (reinterpret_cast <std::byte *>(&idx), sizeof (idx));
383
+ idx = idx % hashes.size ();
384
+ if (f.readRecord (idx, &fail) != hashes[idx]) {
385
+ fail = QString (" Failed to read an individual record at position %1 correctly: %2" ).arg (idx).arg (fail);
386
+ break ;
387
+ }
388
+ ++ctr;
389
+ }
390
+ if (!fail.isEmpty ()) {
391
+ std::unique_lock l (fail_shared_mut);
392
+ fail_shared = fail;
393
+ }
394
+ });
395
+ }
396
+ for (auto & t : thrds) t.join ();
397
+ if (!fail_shared.isEmpty ()) throw Exception (fail_shared);
398
+ Log () << " Read " << ctr.load () << " individual records randomly using " << thrds.size () << " concurrent threads in "
399
+ << t0.msecStr () << " msec" ;
400
+ ++nChecksOK;
401
+ }
402
+ {
403
+ t0 = Tic ();
404
+ // Read 1000 records at a time randomly from 3 threads concurrently
405
+ std::vector<std::thread> thrds;
406
+ RecordFile f (fileName, HashLen);
407
+ if (f.numRecords () != N) throw Exception (" RecordFile has wrong number of records!" );
408
+ QString fail;
409
+ std::atomic_size_t ctr{0 };
410
+ QString fail_shared;
411
+ std::mutex fail_shared_mut;
412
+ for (size_t i = 0 ; i < 3 ; ++i) {
413
+ thrds.emplace_back ([&]{
414
+ QString fail;
415
+ constexpr size_t NBatch = 1000 ;
416
+ for (size_t i = 0 ; fail.isEmpty () && i < hashes.size () * 2 ; i += NBatch) {
417
+ std::vector<uint64_t > recNums (NBatch);
418
+ for (size_t j = 0 ; fail.isEmpty () && j < NBatch; ++j) {
419
+ size_t idx;
420
+ Util::getRandomBytes (reinterpret_cast <std::byte *>(&idx), sizeof (idx));
421
+ idx = idx % N;
422
+ recNums[j] = idx;
423
+ ++ctr;
424
+ }
425
+ const auto results = f.readRandomRecords (recNums, &fail, false );
426
+ if (results.size () != recNums.size ())
427
+ fail = QString (" Failed to read random records: " ) + fail;
428
+ for (size_t i = 0 ; fail.isEmpty () && i < results.size (); ++i) {
429
+ if (results[i] != hashes[recNums[i]])
430
+ fail = QString (" Record #%1, index %2 failed to compare equal!" ).arg (i).arg (recNums[i]);
431
+ }
432
+ }
433
+ if (!fail.isEmpty ()) {
434
+ std::unique_lock l (fail_shared_mut);
435
+ fail_shared = fail;
436
+ }
437
+ });
438
+ }
439
+ for (auto & t : thrds) t.join ();
440
+ if (!fail.isEmpty ()) throw Exception (fail);
441
+ Log () << " Read " << ctr.load () << " batched records randomly using " << thrds.size () << " concurrent threads in "
442
+ << t0.msecStr () << " msec" ;
443
+ ++nChecksOK;
444
+ }
445
+ {
446
+ t0 = Tic ();
447
+ // truncate to N / 2, and verify
448
+ {
449
+ RecordFile f (fileName, HashLen);
450
+ f.truncate (hashes.size () / 2 );
451
+ }
452
+ RecordFile f (fileName, HashLen);
453
+ if (f.numRecords () != hashes.size () / 2 ) throw Exception (" Trunace failed" );
454
+ QString fail;
455
+ const auto results = f.readRecords (0 , hashes.size () / 2 , &fail);
456
+ if (!fail.isEmpty () || results.size () != hashes.size () / 2 ) throw Exception (QString (" Failed to verify truncated data: %1" ).arg (fail));
457
+ for (size_t i = 0 ; i < results.size (); ++i)
458
+ if (results[i] != hashes[i])
459
+ throw Exception (QString (" After truncation, record %1 no longer compares equal!" ).arg (i));
460
+ Log () << " Truncated file to size " << f.numRecords () << " and verified in " << t0.msecStr () << " msec" ;
461
+ ++nChecksOK;
462
+ }
463
+ {
464
+ t0 = Tic ();
465
+ // truncate the file to 0, then write N / 10 records using single-append calls and verify
466
+ {
467
+ RecordFile f (fileName, HashLen);
468
+ f.truncate (0 );
469
+ }
470
+ RecordFile f (fileName, HashLen);
471
+ if (f.numRecords () != 0 ) throw Exception (" Failed to truncate file to 0" );
472
+ const auto NN = hashes.size () / 10 ;
473
+ for (size_t i = 0 ; i < NN; ++i) {
474
+ QString err;
475
+ const auto res = f.appendRecord (hashes[i], true , &err);
476
+ if (!res || *res != i || !err.isEmpty () || f.numRecords () != i + 1 )
477
+ throw Exception (QString (" Failed to append record %1: %2" ).arg (i).arg (err));
478
+ }
479
+ QString fail;
480
+ const auto results = f.readRecords (0 , NN, &fail);
481
+ if (!fail.isEmpty () || results.size () != NN) throw Exception (QString (" Failed to verify truncated data: %1" ).arg (fail));
482
+ for (size_t i = 0 ; i < results.size (); ++i)
483
+ if (results[i] != hashes[i])
484
+ throw Exception (QString (" After truncation, record %1 no longer compares equal!" ).arg (i));
485
+ Log () << " Truncated file to size 0, appended using single-append calls to size " << f.numRecords () << " , and verified in " << t0.msecStr () << " msec" ;
486
+ ++nChecksOK;
487
+ }
488
+ {
489
+ // try mismatch on recSz
490
+ static_assert (!std::is_base_of_v<RecordFile::FileFormatError, Exception>); // to ensure below works.. this is obviously always the case
491
+ try {
492
+ RecordFile f (fileName, HashLen + 1 /* bad recsz */ );
493
+ throw Exception (" Failed to catch expected exception!" );
494
+ } catch (const RecordFile::FileFormatError &e) {
495
+ Log () << " Got expected exception: \" " << e.what () << " \" ok" ;
496
+ }
497
+ ++nChecksOK;
498
+ }
499
+ {
500
+ // try mismatch on magic
501
+ static_assert (!std::is_base_of_v<RecordFile::FileFormatError, Exception>); // to ensure below works.. this is obviously always the case
502
+ try {
503
+ RecordFile f (fileName, HashLen, 0x01020304 /* bad magic */ );
504
+ throw Exception (" Failed to catch expected exception!" );
505
+ } catch (const RecordFile::FileFormatError &e) {
506
+ Log () << " Got expected exception: \" " << e.what () << " \" ok" ;
507
+ }
508
+ ++nChecksOK;
509
+ }
510
+ Log () << nChecksOK << " RecordFile checks passed ok" ;
511
+ }
512
+ const auto test = App::registerTest(" recordfile" , testRecordFile);
513
+ }
514
+ #endif
0 commit comments