From 802388d0746296a244472f275ae5cc8598973f9f Mon Sep 17 00:00:00 2001 From: Andrei Golubev Date: Sat, 24 Apr 2021 01:34:40 +0200 Subject: Add tests for QtPrivate::q_relocate_overlap_n Test the relocation logic through the QADP::relocate() method which basically calls q_relocate_overlap_n inside and then ensures that the data pointers are in good state Running these locally in fact revealed a bug in the implementation, so these tests are definitely good to have Task-number: QTBUG-93019 Change-Id: I353ed46a31c5c77cd0c5fcacd3dfce46e5cf3e67 Reviewed-by: Lars Knoll (cherry picked from commit 65d0f6829cc124f6d0d4003a17bedcb74dddf33b) Reviewed-by: Qt Cherry-pick Bot --- .../corelib/tools/qarraydata/tst_qarraydata.cpp | 281 +++++++++++++++++++++ 1 file changed, 281 insertions(+) (limited to 'tests') diff --git a/tests/auto/corelib/tools/qarraydata/tst_qarraydata.cpp b/tests/auto/corelib/tools/qarraydata/tst_qarraydata.cpp index 1963df1a0f..641f1ec99a 100644 --- a/tests/auto/corelib/tools/qarraydata/tst_qarraydata.cpp +++ b/tests/auto/corelib/tools/qarraydata/tst_qarraydata.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -84,6 +85,10 @@ private slots: void dataPointerAllocate(); void selfEmplaceBackwards(); void selfEmplaceForward(); +#ifndef QT_NO_EXCEPTIONS + void relocateWithExceptions_data(); + void relocateWithExceptions(); +#endif // QT_NO_EXCEPTIONS }; template const T &const_(const T &t) { return t; } @@ -2276,5 +2281,281 @@ void tst_QArrayData::selfEmplaceForward() RUN_TEST_FUNC(testSelfEmplace, MyComplexQString(), 4, complexObjs); } +#ifndef QT_NO_EXCEPTIONS +struct ThrowingTypeWatcher +{ + std::vector destroyedAddrs; + bool watch = false; + + void destroyed(void *addr) + { + if (watch) + destroyedAddrs.push_back(addr); + } +}; + +ThrowingTypeWatcher &throwingTypeWatcher() +{ + static ThrowingTypeWatcher global; + return global; +} + +struct ThrowingType +{ + static unsigned int throwOnce; + static constexpr char throwString[] = "Requested to throw"; + enum MoveCase { + MoveRightNoOverlap, + MoveRightOverlap, + MoveLeftNoOverlap, + MoveLeftOverlap, + }; + enum ThrowCase { + NoThrow, + ThrowInUninitializedRegion, + ThrowInOverlapRegion, + }; + + // reinforce basic checkers with std::shared_ptr which happens to signal + // very explicitly about use-after-free and so on under ASan + std::shared_ptr doubleFreeHelper = std::shared_ptr(new int(42)); + int id = 0; + + void checkThrow() + { + // deferred throw + if (throwOnce > 0) { + --throwOnce; + if (throwOnce == 0) { + throw std::runtime_error(throwString); + } + } + return; + } + + void copy(const ThrowingType &other) noexcept(false) + { + doubleFreeHelper = other.doubleFreeHelper; + id = other.id; + checkThrow(); + } + + ThrowingType(int val = 0) noexcept(false) : id(val) { checkThrow(); } + ThrowingType(const ThrowingType &other) noexcept(false) { copy(other); } + ThrowingType &operator=(const ThrowingType &other) noexcept(false) + { + copy(other); + return *this; + } + ThrowingType(ThrowingType &&other) noexcept(false) { copy(other); } + ThrowingType &operator=(ThrowingType &&other) noexcept(false) + { + copy(other); + return *this; + } + ~ThrowingType() noexcept(true) + { + throwingTypeWatcher().destroyed(this); // notify global watcher + id = -1; + // if we're in dtor but use_count is 0, it's double free + QVERIFY(doubleFreeHelper.use_count() > 0); + } + + friend bool operator==(const ThrowingType &a, const ThrowingType &b) { return a.id == b.id; } +}; + +unsigned int ThrowingType::throwOnce = 0; +static_assert(!QTypeInfo::isRelocatable); + +void tst_QArrayData::relocateWithExceptions_data() +{ + QTest::addColumn("moveCase"); + QTest::addColumn("throwCase"); + // Not throwing + QTest::newRow("no-throw-move-right-no-overlap") + << ThrowingType::MoveRightNoOverlap << ThrowingType::NoThrow; + QTest::newRow("no-throw-move-right-overlap") + << ThrowingType::MoveRightOverlap << ThrowingType::NoThrow; + QTest::newRow("no-throw-move-left-no-overlap") + << ThrowingType::MoveLeftNoOverlap << ThrowingType::NoThrow; + QTest::newRow("no-throw-move-left-overlap") + << ThrowingType::MoveLeftOverlap << ThrowingType::NoThrow; + // Throwing in uninitialized region + QTest::newRow("throw-in-uninit-region-move-right-no-overlap") + << ThrowingType::MoveRightNoOverlap << ThrowingType::ThrowInUninitializedRegion; + QTest::newRow("throw-in-uninit-region-move-right-overlap") + << ThrowingType::MoveRightOverlap << ThrowingType::ThrowInUninitializedRegion; + QTest::newRow("throw-in-uninit-region-move-left-no-overlap") + << ThrowingType::MoveLeftNoOverlap << ThrowingType::ThrowInUninitializedRegion; + QTest::newRow("throw-in-uninit-region-move-left-overlap") + << ThrowingType::MoveLeftOverlap << ThrowingType::ThrowInUninitializedRegion; + // Throwing in overlap region + QTest::newRow("throw-in-overlap-region-move-right-overlap") + << ThrowingType::MoveRightOverlap << ThrowingType::ThrowInOverlapRegion; + QTest::newRow("throw-in-overlap-region-move-left-overlap") + << ThrowingType::MoveLeftOverlap << ThrowingType::ThrowInOverlapRegion; +} + +void tst_QArrayData::relocateWithExceptions() +{ + // Assume that non-throwing moves perform correctly. Otherwise, all previous + // tests would've failed. Test only what happens when exceptions are thrown. + QFETCH(ThrowingType::MoveCase, moveCase); + QFETCH(ThrowingType::ThrowCase, throwCase); + + struct ThrowingTypeLeakChecker + { + ThrowingType::MoveCase moveCase; + ThrowingType::ThrowCase throwCase; + size_t containerSize = 0; + + ThrowingTypeLeakChecker(ThrowingType::MoveCase mc, ThrowingType::ThrowCase tc) + : moveCase(mc), throwCase(tc) + { + } + + void start(qsizetype size) + { + containerSize = size_t(size); + throwingTypeWatcher().watch = true; + } + + ~ThrowingTypeLeakChecker() + { + const size_t destroyedElementsCount = throwingTypeWatcher().destroyedAddrs.size(); + const size_t destroyedElementsUniqueCount = + std::set(throwingTypeWatcher().destroyedAddrs.begin(), + throwingTypeWatcher().destroyedAddrs.end()) + .size(); + + // reset the global watcher first and only then verify things + throwingTypeWatcher().watch = false; + throwingTypeWatcher().destroyedAddrs.clear(); + + size_t deletedByRelocate = 0; + switch (throwCase) { + case ThrowingType::NoThrow: + // if no overlap, N elements from old range. otherwise, N - 1 + // elements from old range + if (moveCase == ThrowingType::MoveLeftNoOverlap + || moveCase == ThrowingType::MoveRightNoOverlap) { + deletedByRelocate = containerSize; + } else { + deletedByRelocate = containerSize - 1; + } + break; + case ThrowingType::ThrowInUninitializedRegion: + // 1 relocated element from uninitialized region + deletedByRelocate = 1u; + break; + case ThrowingType::ThrowInOverlapRegion: + // 2 relocated elements from uninitialized region + deletedByRelocate = 2u; + break; + default: + QFAIL("Unknown throwCase"); + } + + QCOMPARE(destroyedElementsCount, deletedByRelocate + containerSize); + QCOMPARE(destroyedElementsUniqueCount, destroyedElementsCount); + } + }; + + const auto setDeferredThrow = [throwCase]() { + switch (throwCase) { + case ThrowingType::NoThrow: + break; // do nothing + case ThrowingType::ThrowInUninitializedRegion: + ThrowingType::throwOnce = 2; + break; + case ThrowingType::ThrowInOverlapRegion: + ThrowingType::throwOnce = 3; + break; + default: + QFAIL("Unknown throwCase"); + } + }; + + const auto createDataPointer = [](qsizetype capacity, qsizetype initSize) { + QArrayDataPointer qadp(QTypedArrayData::allocate(capacity)); + qadp->appendInitialize(initSize); + int i = 0; + std::generate(qadp.begin(), qadp.end(), [&i]() { return ThrowingType(i++); }); + return qadp; + }; + + switch (moveCase) { + case ThrowingType::MoveRightNoOverlap: { + ThrowingTypeLeakChecker watch(moveCase, throwCase); + auto storage = createDataPointer(20, 3); + QVERIFY(storage.freeSpaceAtEnd() > 3); + + watch.start(storage.size); + try { + setDeferredThrow(); + storage->relocate(4); + if (throwCase != ThrowingType::NoThrow) + QFAIL("Unreachable line!"); + } catch (const std::runtime_error &e) { + QCOMPARE(std::string(e.what()), ThrowingType::throwString); + } + break; + } + case ThrowingType::MoveRightOverlap: { + ThrowingTypeLeakChecker watch(moveCase, throwCase); + auto storage = createDataPointer(20, 3); + QVERIFY(storage.freeSpaceAtEnd() > 3); + + watch.start(storage.size); + try { + setDeferredThrow(); + storage->relocate(2); + if (throwCase != ThrowingType::NoThrow) + QFAIL("Unreachable line!"); + } catch (const std::runtime_error &e) { + QCOMPARE(std::string(e.what()), ThrowingType::throwString); + } + break; + } + case ThrowingType::MoveLeftNoOverlap: { + ThrowingTypeLeakChecker watch(moveCase, throwCase); + auto storage = createDataPointer(20, 2); + storage->insert(0, 1, ThrowingType(42)); + QVERIFY(storage.freeSpaceAtBegin() > 3); + + watch.start(storage.size); + try { + setDeferredThrow(); + storage->relocate(-4); + if (throwCase != ThrowingType::NoThrow) + QFAIL("Unreachable line!"); + } catch (const std::runtime_error &e) { + QCOMPARE(std::string(e.what()), ThrowingType::throwString); + } + break; + } + case ThrowingType::MoveLeftOverlap: { + ThrowingTypeLeakChecker watch(moveCase, throwCase); + auto storage = createDataPointer(20, 2); + storage->insert(0, 1, ThrowingType(42)); + QVERIFY(storage.freeSpaceAtBegin() > 3); + + watch.start(storage.size); + try { + setDeferredThrow(); + storage->relocate(-2); + if (throwCase != ThrowingType::NoThrow) + QFAIL("Unreachable line!"); + } catch (const std::runtime_error &e) { + QCOMPARE(std::string(e.what()), ThrowingType::throwString); + } + break; + } + default: + QFAIL("Unknown ThrowingType::MoveCase"); + }; +} +#endif // QT_NO_EXCEPTIONS + QTEST_APPLESS_MAIN(tst_QArrayData) #include "tst_qarraydata.moc" -- cgit v1.2.3