diff options
| -rw-r--r-- | src/corelib/Qt6AndroidGradleHelpers.cmake | 4 | ||||
| -rw-r--r-- | src/corelib/Qt6AndroidMacros.cmake | 3 | ||||
| -rw-r--r-- | src/corelib/kernel/qcoreapplication.cpp | 16 | ||||
| -rw-r--r-- | src/network/access/qhttp2connection.cpp | 222 | ||||
| -rw-r--r-- | src/network/access/qhttp2connection_p.h | 22 | ||||
| -rw-r--r-- | src/tools/syncqt/main.cpp | 2 | ||||
| -rw-r--r-- | src/widgets/widgets/qtabbar.cpp | 6 | ||||
| -rw-r--r-- | tests/auto/corelib/kernel/qjniobject/tst_qjniobject.cpp | 77 | ||||
| -rw-r--r-- | tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp | 267 | ||||
| -rw-r--r-- | tests/auto/widgets/widgets/qtabbar/tst_qtabbar.cpp | 12 |
10 files changed, 506 insertions, 125 deletions
diff --git a/src/corelib/Qt6AndroidGradleHelpers.cmake b/src/corelib/Qt6AndroidGradleHelpers.cmake index fc8e009b9da..136a61abbcd 100644 --- a/src/corelib/Qt6AndroidGradleHelpers.cmake +++ b/src/corelib/Qt6AndroidGradleHelpers.cmake @@ -6,7 +6,7 @@ function(_qt_internal_android_get_template_path out_var target template_name) if(template_name STREQUAL "") message(FATAL_ERROR "Template name is empty." - " This is the Qt issue, please report a bug at https://bugreports.qt.io.") + " This is a Qt issue, please report a bug at https://bugreports.qt.io.") endif() _qt_internal_android_template_dir(template_directory) @@ -36,7 +36,7 @@ function(_qt_internal_android_get_template_path out_var target template_name) if(template_path STREQUAL "") message(FATAL_ERROR "'${template_name}' is not found." - " This is the Qt issue, please report a bug at https://bugreports.qt.io.") + " This is a Qt issue, please report a bug at https://bugreports.qt.io.") endif() set(${out_var} "${template_path}" PARENT_SCOPE) diff --git a/src/corelib/Qt6AndroidMacros.cmake b/src/corelib/Qt6AndroidMacros.cmake index be362ba1925..ddc531a89f4 100644 --- a/src/corelib/Qt6AndroidMacros.cmake +++ b/src/corelib/Qt6AndroidMacros.cmake @@ -1790,10 +1790,11 @@ function(_qt_internal_android_app_runner_arguments target out_runner_path out_ar set(${out_runner_path} "${runner_dir}/qt-android-runner.py" PARENT_SCOPE) _qt_internal_android_get_target_android_build_dir(android_build_dir ${target}) + _qt_internal_android_get_target_deployment_dir(android_deployment_dir ${target}) _qt_internal_android_get_platform_tools_path(platform_tools) set(${out_arguments} "--adb" "${platform_tools}/adb" - "--build-path" "${android_build_dir}" + "--build-path" "${android_deployment_dir}" "--apk" "${android_build_dir}/${target}.apk" PARENT_SCOPE ) diff --git a/src/corelib/kernel/qcoreapplication.cpp b/src/corelib/kernel/qcoreapplication.cpp index afc85fe36fb..19f5596880a 100644 --- a/src/corelib/kernel/qcoreapplication.cpp +++ b/src/corelib/kernel/qcoreapplication.cpp @@ -2913,7 +2913,6 @@ void QCoreApplication::requestPermissionImpl(const QPermission &requestedPermiss void *args[] = { nullptr, const_cast<QPermission *>(&permission) }; slotObject->call(const_cast<QObject *>(context.data()), args); } - deleteLater(); } private: @@ -2921,9 +2920,11 @@ void QCoreApplication::requestPermissionImpl(const QPermission &requestedPermiss QPointer<const QObject> context; }; - PermissionReceiver *receiver = new PermissionReceiver(std::move(slotObj), context); + // ### use unique_ptr once PermissionCallback is a move_only function + auto receiver = std::make_shared<PermissionReceiver>(std::move(slotObj), context); - QPermissions::Private::requestPermission(requestedPermission, [=](Qt::PermissionStatus status) { + QPermissions::Private::requestPermission(requestedPermission, + [=, receiver = std::move(receiver)](Qt::PermissionStatus status) mutable { if (status == Qt::PermissionStatus::Undetermined) { Q_ASSERT_X(false, "QPermission", "Internal error: requestPermission() should never return Undetermined"); @@ -2933,10 +2934,11 @@ void QCoreApplication::requestPermissionImpl(const QPermission &requestedPermiss if (QCoreApplication::self) { QPermission permission = requestedPermission; permission.m_status = status; - QMetaObject::invokeMethod(receiver, - &PermissionReceiver::finalizePermissionRequest, - Qt::QueuedConnection, - permission); + auto receiverObject = receiver.get(); + QMetaObject::invokeMethod(receiverObject, + [receiver = std::move(receiver), permission] { + receiver->finalizePermissionRequest(permission); + }, Qt::QueuedConnection); } }); } diff --git a/src/network/access/qhttp2connection.cpp b/src/network/access/qhttp2connection.cpp index c0b07ddd652..62afd172d57 100644 --- a/src/network/access/qhttp2connection.cpp +++ b/src/network/access/qhttp2connection.cpp @@ -15,6 +15,7 @@ #include <algorithm> #include <memory> +#include <chrono> QT_BEGIN_NAMESPACE @@ -646,6 +647,8 @@ void QHttp2Stream::setState(State newState) streamID(), int(m_state), int(newState)); m_state = newState; emit stateChanged(newState); + if (m_state == State::Closed) + getConnection()->maybeCloseOnGoingAway(); } // Changes the state as appropriate given the current state and the transition. @@ -1033,6 +1036,27 @@ QHttp2Stream *QHttp2Connection::getStream(quint32 streamID) const return m_streams.value(streamID, nullptr).get(); } +/*! + Initiates connection shutdown. When \a errorCode is \c{NO_ERROR}, graceful + shutdown is initiated, allowing existing streams to complete. Otherwise the + connection is closed immediately with an error. +*/ +void QHttp2Connection::close(Http2::Http2Error errorCode) +{ + if (m_connectionAborted) + return; + + if (errorCode == Http2::HTTP2_NO_ERROR) { + if (m_connectionType == Type::Server) + sendInitialServerGracefulShutdownGoaway(); + else + sendClientGracefulShutdownGoaway(); + } else { + // RFC 9113, 5.4.1: After sending the GOAWAY frame for an error + // condition, the endpoint MUST close the TCP connection + connectionError(errorCode, "Connection closed with error", false); + } +} /*! \fn QHttp2Stream *QHttp2Connection::promisedStream(const QUrl &streamKey) const @@ -1042,13 +1066,6 @@ QHttp2Stream *QHttp2Connection::getStream(quint32 streamID) const */ /*! - \fn void QHttp2Connection::close() - - This sends a GOAWAY frame on the connection stream, gracefully closing the - connection. -*/ - -/*! \fn bool QHttp2Connection::isGoingAway() const noexcept Returns \c true if the connection is in the process of being closed, or @@ -1154,13 +1171,6 @@ void QHttp2Connection::handleReadyRead() if (m_connectionType == Type::Server && !serverCheckClientPreface()) return; - const auto streamIsActive = [](const QPointer<QHttp2Stream> &stream) { - return stream && stream->isActive(); - }; - if (m_goingAway && std::none_of(m_streams.cbegin(), m_streams.cend(), streamIsActive)) { - close(); - return; - } QIODevice *socket = getSocket(); qCDebug(qHttp2ConnectionLog, "[%p] Receiving data, %lld bytes available", this, @@ -1170,13 +1180,13 @@ void QHttp2Connection::handleReadyRead() if (!m_prefaceSent) return; - while (!m_goingAway || std::any_of(m_streams.cbegin(), m_streams.cend(), streamIsActive)) { + while (!m_connectionAborted) { const auto result = frameReader.read(*socket); if (result != FrameStatus::goodFrame) qCDebug(qHttp2ConnectionLog, "[%p] Tried to read frame, got %d", this, int(result)); switch (result) { case FrameStatus::incompleteFrame: - return; + return; // No more complete frames available case FrameStatus::protocolError: return connectionError(PROTOCOL_ERROR, "invalid frame"); case FrameStatus::sizeError: { @@ -1297,24 +1307,35 @@ void QHttp2Connection::setH2Configuration(QHttp2Configuration config) encoder.setCompressStrings(m_config.huffmanCompressionEnabled()); } -void QHttp2Connection::connectionError(Http2Error errorCode, const char *message) +void QHttp2Connection::connectionError(Http2Error errorCode, const char *message, bool logAsError) { Q_ASSERT(message); - // RFC 9113, 6.8: An endpoint MAY send multiple GOAWAY frames if circumstances change. + if (m_connectionAborted) + return; + m_connectionAborted = true; - qCCritical(qHttp2ConnectionLog, "[%p] Connection error: %s (%d)", this, message, - int(errorCode)); + if (logAsError) { + qCCritical(qHttp2ConnectionLog, "[%p] Connection error: %s (%d)", this, message, + int(errorCode)); + } else { + qCDebug(qHttp2ConnectionLog, "[%p] Closing connection: %s (%d)", this, message, + int(errorCode)); + } - // RFC 9113, 6.8: Endpoints SHOULD always send a GOAWAY frame before closing a connection so - // that the remote peer can know whether a stream has been partially processed or not. - sendGOAWAY(errorCode); + // Mark going away so other code paths will stop creating new streams + m_goingAway = true; + // RFC 9113 5.4.1: An endpoint that encounters a connection error SHOULD + // first send a GOAWAY frame with the last incoming stream ID. + m_lastStreamToProcess = std::min(m_lastIncomingStreamID, m_lastStreamToProcess); + sendGOAWAYFrame(errorCode, m_lastStreamToProcess); auto messageView = QLatin1StringView(message); for (QHttp2Stream *stream : std::as_const(m_streams)) { if (stream && stream->isActive()) stream->finishWithError(errorCode, messageView); } - + // RFC 9113 5.4.1: After sending the GOAWAY frame for an error condition, + // the endpoint MUST close the TCP connection closeSession(); } @@ -1423,20 +1444,99 @@ bool QHttp2Connection::sendWINDOW_UPDATE(quint32 streamID, quint32 delta) return frameWriter.write(*getSocket()); } -bool QHttp2Connection::sendGOAWAY(Http2::Http2Error errorCode) +void QHttp2Connection::sendClientGracefulShutdownGoaway() { + // Clients send a single GOAWAY. No race condition since they control stream creation + Q_ASSERT(m_connectionType == Type::Client); + + if (m_connectionAborted || m_goingAway) { + qCWarning(qHttp2ConnectionLog, "[%p] Client graceful shutdown already in progress", this); + return; + } + m_goingAway = true; - // If this is the first time, start the timer: - if (m_lastStreamToProcess == Http2::lastValidStreamID) - m_goawayGraceTimer.setRemainingTime(GoawayGracePeriod); - m_lastStreamToProcess = std::min(m_lastIncomingStreamID, m_lastStreamToProcess); + m_gracefulShutdownState = GracefulShutdownState::FinalGOAWAYSent; + m_lastStreamToProcess = m_lastIncomingStreamID; + sendGOAWAYFrame(Http2::HTTP2_NO_ERROR, m_lastStreamToProcess); + + maybeCloseOnGoingAway(); +} + +void QHttp2Connection::sendInitialServerGracefulShutdownGoaway() +{ + Q_ASSERT(m_connectionType == Type::Server); + // RFC 9113, 6.8: A server that is attempting to gracefully shut down a + // connection SHOULD send an initial GOAWAY frame with the last stream + // identifier set to 2^31-1 and a NO_ERROR code. + if (m_connectionAborted || m_goingAway) { + qCWarning(qHttp2ConnectionLog, "[%p] Server graceful shutdown already in progress", this); + return; + } + + m_goingAway = true; + m_goawayGraceTimer.setRemainingTime(GoawayGracePeriod); + sendGOAWAYFrame(Http2::HTTP2_NO_ERROR, Http2::lastValidStreamID); + + // Send PING to measure RTT; handlePING() continues the shutdown on ACK. + // RFC 9113 6.8: After allowing time for any in-flight stream creation + // (at least one round-trip time) + if (sendPing()) + m_gracefulShutdownState = GracefulShutdownState::AwaitingShutdownPing; + else + m_gracefulShutdownState = GracefulShutdownState::AwaitingPriorPing; +} + +void QHttp2Connection::sendFinalServerGracefulShutdownGoaway() +{ + if (m_connectionAborted || !m_goingAway) { + qCWarning(qHttp2ConnectionLog, "[%p] Server graceful shutdown not in progress", this); + return; + } + m_gracefulShutdownState = GracefulShutdownState::FinalGOAWAYSent; + m_lastStreamToProcess = m_lastIncomingStreamID; + sendGOAWAYFrame(Http2::HTTP2_NO_ERROR, m_lastStreamToProcess); + maybeCloseOnGoingAway(); +} + +bool QHttp2Connection::sendGOAWAYFrame(Http2::Http2Error errorCode, quint32 lastStreamID) +{ + QIODevice *socket = getSocket(); + if (!socket || !socket->isOpen()) + return false; + qCDebug(qHttp2ConnectionLog, "[%p] Sending GOAWAY frame, error code %u, last stream %u", this, - errorCode, m_lastStreamToProcess); + errorCode, lastStreamID); + frameWriter.start(FrameType::GOAWAY, FrameFlag::EMPTY, Http2PredefinedParameters::connectionStreamID); - frameWriter.append(m_lastStreamToProcess); + frameWriter.append(lastStreamID); frameWriter.append(quint32(errorCode)); - return frameWriter.write(*getSocket()); + return frameWriter.write(*socket); +} + +void QHttp2Connection::maybeCloseOnGoingAway() +{ + // Only close if we've reached the final phase of graceful shutdown + // For the sender: after FinalGOAWAYSent + // For the receiver: after receiving GOAWAY and all our streams are done + if (m_connectionAborted || !m_goingAway) { + qCDebug(qHttp2ConnectionLog, "[%p] Connection close deferred, graceful shutdown not active", + this); + return; + } + + // For graceful shutdown initiator, only close after final GOAWAY is sent + if (m_gracefulShutdownState == GracefulShutdownState::AwaitingShutdownPing) + return; // Still waiting for RTT measurement before final GOAWAY + + const auto streamIsActive = [](const QPointer<QHttp2Stream> &stream) { + return stream && stream->isActive(); + }; + + if (std::none_of(m_streams.cbegin(), m_streams.cend(), streamIsActive)) { + qCDebug(qHttp2ConnectionLog, "[%p] All streams closed, closing connection", this); + closeSession(); + } } bool QHttp2Connection::sendSETTINGS_ACK() @@ -1571,8 +1671,6 @@ void QHttp2Connection::handleHEADERS() qCDebug(qHttp2ConnectionLog, "[%p] HEADERS frame on stream %d has PRIORITY flag", this, streamID); handlePRIORITY(); - if (m_goingAway) - return; } const bool endHeaders = flags.testFlag(FrameFlag::END_HEADERS); @@ -1834,6 +1932,17 @@ void QHttp2Connection::handlePING() emit pingFrameReceived(PingState::PongSignatureIdentical); } m_lastPingSignature.reset(); + + // Handle sendInitialServerGracefulShutdownGoaway() + if (m_gracefulShutdownState == GracefulShutdownState::AwaitingShutdownPing) { + sendFinalServerGracefulShutdownGoaway(); + } else if (m_gracefulShutdownState == GracefulShutdownState::AwaitingPriorPing) { + // Prior PING completed, now send our RTT measurement PING. This shouldn't fail! + m_gracefulShutdownState = GracefulShutdownState::AwaitingShutdownPing; + [[maybe_unused]] const bool ok = sendPing(); + Q_ASSERT(ok); + } + return; } else { emit pingFrameReceived(PingState::Ping); @@ -1876,27 +1985,44 @@ void QHttp2Connection::handleGOAWAY() if (lastStreamID != 0 && (lastStreamID & 0x1) != LocalMask) return connectionError(PROTOCOL_ERROR, "GOAWAY with invalid last stream ID"); + // 6.8 - An endpoint MAY send multiple GOAWAY frames if circumstances + // change. Endpoints MUST NOT increase the value they send in the last + // stream identifier + if (m_lastGoAwayLastStreamID && lastStreamID > *m_lastGoAwayLastStreamID) + return connectionError(PROTOCOL_ERROR, "Repeated GOAWAY with invalid last stream ID"); + m_lastGoAwayLastStreamID = lastStreamID; + qCDebug(qHttp2ConnectionLog, "[%p] Received GOAWAY frame, error code %u, last stream %u", this, errorCode, lastStreamID); m_goingAway = true; emit receivedGOAWAY(errorCode, lastStreamID); - // Since the embedded stream ID is the last one that was or _might be_ processed, - // we cancel anything that comes after it. 0 can be used in the special case that - // no streams at all were or will be processed. - const quint32 firstPossibleStream = m_connectionType == Type::Client ? 1 : 2; - const quint32 firstCancelledStream = lastStreamID ? lastStreamID + 2 : firstPossibleStream; - Q_ASSERT((firstCancelledStream & 0x1) == LocalMask); - for (quint32 id = firstCancelledStream; id < m_nextStreamID; id += 2) { - QHttp2Stream *stream = m_streams.value(id, nullptr); - if (stream && stream->isActive()) - stream->finishWithError(errorCode, "Received GOAWAY"_L1); - } - - const auto isActive = [](const QHttp2Stream *stream) { return stream && stream->isActive(); }; - if (std::none_of(m_streams.cbegin(), m_streams.cend(), isActive)) + if (errorCode == HTTP2_NO_ERROR) { + // Graceful GOAWAY (NO_ERROR): Only cancel streams the peer explicitly won't process + // (those with IDs > lastStreamID). Streams with ID <= lastStreamID can still complete. + // '0' can be used in the special case that no streams at all were or will be processed. + const quint32 firstPossibleStream = m_connectionType == Type::Client ? 1 : 2; + const quint32 firstCancelledStream = lastStreamID ? lastStreamID + 2 : firstPossibleStream; + Q_ASSERT((firstCancelledStream & 0x1) == LocalMask); + for (quint32 id = firstCancelledStream; id < m_nextStreamID; id += 2) { + QHttp2Stream *stream = m_streams.value(id, nullptr); + if (stream && stream->isActive()) + stream->finishWithError(errorCode, "Received GOAWAY"_L1); + } + maybeCloseOnGoingAway(); // check if we can close now + } else { + // RFC 9113, 5.4.1: After sending the GOAWAY frame for an error + // condition, the endpoint MUST close the TCP connection. + // As the peer is closing the connection immediately, they won't + // process any more data, so we close the connection here already. + m_connectionAborted = true; + for (QHttp2Stream *stream : std::as_const(m_streams)) { + if (stream && stream->isActive()) + stream->finishWithError(errorCode, "Received GOAWAY"_L1); + } closeSession(); + } } void QHttp2Connection::handleWINDOW_UPDATE() diff --git a/src/network/access/qhttp2connection_p.h b/src/network/access/qhttp2connection_p.h index e2af7d7ab33..d9f2bccc58a 100644 --- a/src/network/access/qhttp2connection_p.h +++ b/src/network/access/qhttp2connection_p.h @@ -260,7 +260,7 @@ public: return nullptr; } - void close(Http2::Http2Error error = Http2::HTTP2_NO_ERROR) { sendGOAWAY(error); } + void close(Http2::Http2Error errorCode = Http2::HTTP2_NO_ERROR); bool isGoingAway() const noexcept { return m_goingAway; } @@ -302,8 +302,7 @@ private: Q_ALWAYS_INLINE bool streamIsIgnored(quint32 streamID) const noexcept; - void connectionError(Http2::Http2Error errorCode, - const char *message); // Connection failed to be established? + void connectionError(Http2::Http2Error errorCode, const char *message, bool logAsError = true); void setH2Configuration(QHttp2Configuration config); void closeSession(); void registerStreamAsResetLocally(quint32 streamID); @@ -316,7 +315,11 @@ private: bool sendServerPreface(); bool serverCheckClientPreface(); bool sendWINDOW_UPDATE(quint32 streamID, quint32 delta); - bool sendGOAWAY(Http2::Http2Error errorCode); + void sendClientGracefulShutdownGoaway(); + void sendInitialServerGracefulShutdownGoaway(); + void sendFinalServerGracefulShutdownGoaway(); + bool sendGOAWAYFrame(Http2::Http2Error errorCode, quint32 lastSreamID); + void maybeCloseOnGoingAway(); bool sendSETTINGS_ACK(); void handleDATA(); @@ -423,6 +426,17 @@ private: static constexpr std::chrono::duration GoawayGracePeriod = std::chrono::seconds(60); QDeadlineTimer m_goawayGraceTimer; + std::optional<quint32> m_lastGoAwayLastStreamID; + bool m_connectionAborted = false; + + enum class GracefulShutdownState { + None, + AwaitingPriorPing, + AwaitingShutdownPing, + FinalGOAWAYSent, + }; + GracefulShutdownState m_gracefulShutdownState = GracefulShutdownState::None; + bool m_prefaceSent = false; // Server-side only: diff --git a/src/tools/syncqt/main.cpp b/src/tools/syncqt/main.cpp index cfef5be7e2f..3ec70bcdfcd 100644 --- a/src/tools/syncqt/main.cpp +++ b/src/tools/syncqt/main.cpp @@ -1673,7 +1673,7 @@ public: void SyncScanner::updateSymbolDescriptor(const std::string &symbol, const std::string &file, SymbolDescriptor::SourceType type) { - if (m_commandLineArgs->showOnly()) + if (m_commandLineArgs->showOnly() || m_commandLineArgs->debug()) std::cout << " SYMBOL: " << symbol << std::endl; m_symbols[symbol].update(file, type); } diff --git a/src/widgets/widgets/qtabbar.cpp b/src/widgets/widgets/qtabbar.cpp index 44218d41ded..84fe8c3f44a 100644 --- a/src/widgets/widgets/qtabbar.cpp +++ b/src/widgets/widgets/qtabbar.cpp @@ -834,7 +834,7 @@ void QTabBarPrivate::scrollTabs() const auto &tabRect = tab->rect; int start = horizontal ? tabRect.left() : tabRect.top(); int end = horizontal ? tabRect.right() : tabRect.bottom(); - if (end > scrollRect.right() && start > scrollOffset) { + if (end > scrollRect.right() && start > scrollRect.left()) { makeVisible(i); return; } @@ -2449,10 +2449,12 @@ void QTabBar::wheelEvent(QWheelEvent *event) if (!d->rightB->isVisible()) scrollRectExtent += tabsVertical ? d->rightB->height() : d->rightB->width(); + const QRect scrollRect0 = d->normalizedScrollRect(0); + const int minScrollOffset = -1 * scrollRect0.left(); const int maxScrollOffset = qMax((tabsVertical ? lastTabRect.bottom() : lastTabRect.right()) - scrollRectExtent, 0); - d->scrollOffset = qBound(0, d->scrollOffset - delta, maxScrollOffset); + d->scrollOffset = qBound(minScrollOffset, d->scrollOffset - delta, maxScrollOffset); d->leftB->setEnabled(d->scrollOffset > -scrollRect.left()); d->rightB->setEnabled(maxScrollOffset > d->scrollOffset); if (oldScrollOffset != d->scrollOffset) { diff --git a/tests/auto/corelib/kernel/qjniobject/tst_qjniobject.cpp b/tests/auto/corelib/kernel/qjniobject/tst_qjniobject.cpp index 215b3bf3b78..af6a6a41efe 100644 --- a/tests/auto/corelib/kernel/qjniobject/tst_qjniobject.cpp +++ b/tests/auto/corelib/kernel/qjniobject/tst_qjniobject.cpp @@ -10,8 +10,19 @@ #if defined(__cpp_lib_expected) # include <expected> -#else -# include <QtCore/private/qexpected_p.h> + +template <typename T> +using QJniReturnValue = std::expected<T, jthrowable>; +using BadAccessException = std::bad_expected_access<jthrowable>; + +// even with __cpp_lib_expected >= 202211L, monadic functions seem to be rather +// broken or not reliably available +#define EXPECTED_HAS_MONADIC (__cpp_lib_expected >= 202211L) + +static_assert(QtJniTypes::Traits<QJniReturnValue<int>>::signature() == + QtJniTypes::Traits<int>::signature()); +static_assert(QtJniTypes::Traits<QJniReturnValue<QString>>::signature() == + QtJniTypes::Traits<QString>::signature()); #endif QT_BEGIN_NAMESPACE @@ -2510,35 +2521,12 @@ void tst_QJniObject::implicitExceptionHandling_setStaticField() void>); } -#if __cpp_lib_expected -template <typename T> -using QJniReturnValue = std::expected<T, jthrowable>; -using BadAccessException = std::bad_expected_access<jthrowable>; -// even with __cpp_lib_expected >= 202211L, monadic functions seem to be rather -// broken or not reliably available -#define EXPECTED_HAS_MONADIC false -#elif TL_EXPECTED_VERSION_MAJOR -#define EXPECTED_HAS_MONADIC true -template <typename T> -using QJniReturnValue = tl::expected<T, jthrowable>; -using BadAccessException = tl::bad_expected_access<jthrowable>; -#endif - -static_assert(QtJniTypes::Traits<QJniReturnValue<int>>::signature() == - QtJniTypes::Traits<int>::signature()); -static_assert(QtJniTypes::Traits<QJniReturnValue<QString>>::signature() == - QtJniTypes::Traits<QString>::signature()); - - void tst_QJniObject::constructWithException() { -#if __cpp_lib_expected - qInfo() << "Testing explicit exception handling with std::expected" << __cpp_lib_expected; -#elif defined(TL_EXPECTED_VERSION_MAJOR) - qInfo() << "Testing explicit exception handling with tl::expected"; +#ifndef __cpp_lib_expected + QSKIP("std::expected not available."); #else - qInfo() << "Testing explicit exception handling with QJniReturnValue"; -#endif + qInfo() << "Testing explicit exception handling with std::expected" << __cpp_lib_expected; const QRegularExpression invalidClass("java.lang.ClassNotFoundException: .*"); { @@ -2575,10 +2563,14 @@ void tst_QJniObject::constructWithException() } QVERIFY(!QJniEnvironment().checkAndClearExceptions()); +#endif } void tst_QJniObject::callMethodWithException() { +#ifndef __cpp_lib_expected + QSKIP("std::expected not available."); +#else TestClass testObject; { auto result = testObject.callMethod<QJniReturnValue<void>>("voidMethod"); @@ -2640,11 +2632,14 @@ void tst_QJniObject::callMethodWithException() QCOMPARE_GE(stackTrace.size(), 1); QCOMPARE(stackTrace.at(0), u"java.lang.Throwable: "_s + A_STRING_OBJECT()); } +#endif } void tst_QJniObject::callMethodWithMonadic() { -#if !EXPECTED_HAS_MONADIC +#ifndef __cpp_lib_expected + QSKIP("std::expected not available."); +#elif !EXPECTED_HAS_MONADIC QSKIP("Used version of std::expected does not have monadic functions"); #else enum Monadic { @@ -2750,6 +2745,9 @@ void tst_QJniObject::callMethodWithMonadic() void tst_QJniObject::callMethodWithTryCatch() { +#ifndef __cpp_lib_expected + QSKIP("std::expected not available."); +#else TestClass testObject; const QRegularExpression invalidMethod("java.lang.NoSuchMethodError: .*"); @@ -2762,10 +2760,14 @@ void tst_QJniObject::callMethodWithTryCatch() catch (BadAccessException &e) { qWarning().noquote() << QJniEnvironment::stackTrace(e.error()).join('\n'); } +#endif } void tst_QJniObject::callStaticMethodWithException() { +#ifndef __cpp_lib_expected + QSKIP("std::expected not available."); +#else { auto result = TestClass::callStaticMethod<QJniReturnValue<int>>("staticIntMethod"); QVERIFY(result); @@ -2790,10 +2792,14 @@ void tst_QJniObject::callStaticMethodWithException() QCOMPARE_GE(stackTrace.size(), 1); QCOMPARE(stackTrace.at(0), u"java.lang.Throwable: "_s + A_STRING_OBJECT()); } +#endif } void tst_QJniObject::getFieldWithException() { +#ifndef __cpp_lib_expected + QSKIP("std::expected not available."); +#else TestClass testObject; { auto result = testObject.getField<QJniReturnValue<jboolean>>("BOOL_FIELD"); @@ -2808,10 +2814,14 @@ void tst_QJniObject::getFieldWithException() result = testObject.getField<QJniReturnValue<QString>>("INVALID_STRING"); QVERIFY(!result && result.error()); } +#endif } void tst_QJniObject::setFieldWithException() { +#ifndef __cpp_lib_expected + QSKIP("std::expected not available."); +#else TestClass testObject; { auto result = testObject.setField<QJniReturnValue<jboolean>>("BOOL_FIELD", true); @@ -2828,10 +2838,14 @@ void tst_QJniObject::setFieldWithException() QVERIFY(!result); QVERIFY(result.error()); } +#endif } void tst_QJniObject::getStaticFieldWithException() { +#ifndef __cpp_lib_expected + QSKIP("std::expected not available."); +#else { auto result = TestClass::getStaticField<QJniReturnValue<jshort>>("S_SHORT_VAR"); QVERIFY(result); @@ -2847,10 +2861,14 @@ void tst_QJniObject::getStaticFieldWithException() QVERIFY(!result); QVERIFY(result.error()); } +#endif } void tst_QJniObject::setStaticFieldWithException() { +#ifndef __cpp_lib_expected + QSKIP("std::expected not available."); +#else { auto result = TestClass::setStaticField<QJniReturnValue<jboolean>>("S_BOOLEAN_VAR", true); QVERIFY(result); @@ -2866,6 +2884,7 @@ void tst_QJniObject::setStaticFieldWithException() QVERIFY(!result); QVERIFY(result.error()); } +#endif } QTEST_MAIN(tst_QJniObject) diff --git a/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp b/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp index 22aa9d44262..547dc9de4f7 100644 --- a/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp +++ b/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp @@ -9,6 +9,7 @@ #include <QtNetwork/private/bitstreams_p.h> #include <QtCore/qregularexpression.h> +#include <QtCore/qthread.h> #include <limits> @@ -41,6 +42,9 @@ private slots: void testCONTINUATIONFrame(); void goaway_data(); void goaway(); + void serverInitiatedGoaways_data(); + void serverInitiatedGoaways(); + void clientInitiatedGoaway(); private: enum PeerType { Client, Server }; @@ -1132,10 +1136,10 @@ void tst_QHttp2Connection::goaway() QVERIFY(waitForSettingsExchange(connection, serverConnection)); QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream }; - - QSignalSpy clientIncomingStreamSpy{ connection, &QHttp2Connection::newIncomingStream }; - QSignalSpy clientHeaderReceivedSpy{ clientStream, &QHttp2Stream::headersReceived }; QSignalSpy clientGoawaySpy{ connection, &QHttp2Connection::receivedGOAWAY }; + QSignalSpy clientClosedSpy{ connection, &QHttp2Connection::connectionClosed }; + QSignalSpy serverGoawaySpy{ serverConnection, &QHttp2Connection::receivedGOAWAY }; + QSignalSpy serverClosedSpy{ serverConnection, &QHttp2Connection::connectionClosed }; const HPack::HttpHeader headers = getRequiredHeaders(); clientStream->sendHEADERS(headers, false); @@ -1143,57 +1147,78 @@ void tst_QHttp2Connection::goaway() QVERIFY(newIncomingStreamSpy.wait()); auto *serverStream = newIncomingStreamSpy.front().front().value<QHttp2Stream *>(); QVERIFY(serverStream); - QVERIFY(serverConnection->sendGOAWAY(Http2::CANCEL)); + serverConnection->close(); // NO_ERROR -> graceful shutdown + + // New stream creation is illegal now auto createStreamResult = serverConnection->createLocalStreamInternal(); QVERIFY(createStreamResult.has_error()); QCOMPARE(createStreamResult.error(), QHttp2Connection::CreateStreamError::ReceivedGOAWAY); + // Client received initial GOAWAY QVERIFY(clientGoawaySpy.wait()); - QCOMPARE(clientGoawaySpy.size(), 1); - // The error code used: - QCOMPARE(clientGoawaySpy.first().first().value<Http2::Http2Error>(), Http2::CANCEL); - // Last ID that will be processed - QCOMPARE(clientGoawaySpy.first().last().value<quint32>(), clientStream->streamID()); + QCOMPARE(clientGoawaySpy.first().first().value<Http2::Http2Error>(), Http2::HTTP2_NO_ERROR); + QCOMPARE(clientGoawaySpy.first().last().value<quint32>(), Http2::lastValidStreamID); clientGoawaySpy.clear(); - // Test that creating a stream the normal way results in an error: - QH2Expected<QHttp2Stream *, QHttp2Connection::CreateStreamError> - invalidStream = connection->createStream(); + // New client-stream creation is illegal now + auto invalidStream = connection->createStream(); QVERIFY(!invalidStream.ok()); QVERIFY(invalidStream.has_error()); QCOMPARE(invalidStream.error(), QHttp2Connection::CreateStreamError::ReceivedGOAWAY); + // Client receives final GOAWAY with actual lastStreamID (after PING RTT) + QVERIFY(clientGoawaySpy.wait()); + QCOMPARE(clientGoawaySpy.first().at(0).value<Http2::Http2Error>(), Http2::HTTP2_NO_ERROR); + QCOMPARE(clientGoawaySpy.first().at(1).value<quint32>(), clientStream->streamID()); + clientGoawaySpy.clear(); + // Directly create a stream to avoid the GOAWAY check: quint32 nextStreamId = clientStream->streamID() + 2; - QHttp2Stream *secondClientStream = connection->createStreamInternal_impl(nextStreamId); - QSignalSpy streamResetSpy{ secondClientStream, &QHttp2Stream::rstFrameReceived }; - secondClientStream->sendHEADERS(headers, endStreamOnHEADERS); + QHttp2Stream *ignoredClientStream = connection->createStreamInternal_impl(nextStreamId); + QSignalSpy streamResetSpy{ ignoredClientStream, &QHttp2Stream::rstFrameReceived }; + ignoredClientStream->sendHEADERS(headers, endStreamOnHEADERS); // The stream should be ignored: using namespace std::chrono_literals; QVERIFY(!streamResetSpy.wait(100ms)); // We don't get reset because we are ignored if (endStreamOnHEADERS) return; - secondClientStream->sendDATA("my data", createNewStreamAfterDelay); + ignoredClientStream->sendDATA("my data", createNewStreamAfterDelay); // We cheat and try to send data after the END_STREAM flag has been sent if (!createNewStreamAfterDelay) { // Manually send a frame with END_STREAM so the QHttp2Stream thinks it's fine to send more // DATA connection->frameWriter.start(Http2::FrameType::DATA, Http2::FrameFlag::END_STREAM, - secondClientStream->streamID()); + ignoredClientStream->streamID()); connection->frameWriter.write(*connection->getSocket()); QVERIFY(!streamResetSpy.wait(100ms)); // We don't get reset because we are ignored - // Even without the GOAWAY this should fail (more activity after END_STREAM) - secondClientStream->sendDATA("my data", true); - QTest::ignoreMessage(QtCriticalMsg, - QRegularExpression(u".*Connection error: DATA on invalid stream.*"_s)); - QVERIFY(clientGoawaySpy.wait()); - QCOMPARE(clientGoawaySpy.size(), 1); - QCOMPARE(clientGoawaySpy.first().first().value<Http2::Http2Error>(), + const auto tstStream = [](const auto &spy, Http2::Http2Error code, + QLatin1StringView errMsg) { + QCOMPARE(spy.first().at(0).template value<Http2::Http2Error>(), code); + QCOMPARE(spy.first().at(1).template value<QString>(), errMsg); + }; + QLatin1StringView serverErrorMsg("DATA on invalid stream"); + QTest::ignoreMessage(QtCriticalMsg, QRegularExpression(".*" + serverErrorMsg + ".*")); + QSignalSpy clientStreamErrorSpy(clientStream, &QHttp2Stream::errorOccurred); + QSignalSpy secondclientStreamErrorSpy(ignoredClientStream, &QHttp2Stream::errorOccurred); + QSignalSpy serverStreamErrorSpy(serverStream, &QHttp2Stream::errorOccurred); + + // Triggers a connectionError of 'ENHANCE_YOUR_CALM' on the server + // (more activity after END_STREAM) + ignoredClientStream->sendDATA("my data", true); + QTRY_COMPARE(serverClosedSpy.count(), 1); + tstStream(serverStreamErrorSpy, Http2::ENHANCE_YOUR_CALM, serverErrorMsg); + + QTRY_COMPARE(clientGoawaySpy.count(), 1); + QCOMPARE(clientGoawaySpy.first().at(0).value<Http2::Http2Error>(), Http2::ENHANCE_YOUR_CALM); - QCOMPARE(clientGoawaySpy.first().last().value<quint32>(), clientStream->streamID()); - return; // connection is dead by now + QCOMPARE(clientGoawaySpy.first().at(1).value<quint32>(), clientStream->streamID()); + QTRY_COMPARE(clientClosedSpy.count(), 1); + QLatin1StringView clientErrorMsg("Received GOAWAY"); + tstStream(clientStreamErrorSpy, Http2::ENHANCE_YOUR_CALM, clientErrorMsg); + tstStream(secondclientStreamErrorSpy, Http2::ENHANCE_YOUR_CALM, clientErrorMsg); + return; } // Override the deadline timer so we don't have to wait too long @@ -1216,6 +1241,196 @@ void tst_QHttp2Connection::goaway() QCOMPARE(clientGoawaySpy.first().last().value<quint32>(), clientStream->streamID()); } +void tst_QHttp2Connection::serverInitiatedGoaways_data() +{ + QTest::addColumn<QString>("scenario"); + + QTest::newRow("graceful-shutdown") << u"graceful-shutdown"_s; + QTest::newRow("graceful-then-error") << u"graceful-then-error"_s; + QTest::newRow("increasing-lastStreamId") << u"increasing-lastStreamId"_s; +} + +void tst_QHttp2Connection::serverInitiatedGoaways() +{ + QFETCH(QString, scenario); + + auto [client, server] = makeFakeConnectedSockets(); + auto clientConn = makeHttp2Connection(client.get(), {}, Client); + auto serverConn = makeHttp2Connection(server.get(), {}, Server); + + // Client creates stream + auto *clientStream = clientConn->createStream().unwrap(); + QVERIFY(clientStream); + QVERIFY(waitForSettingsExchange(clientConn, serverConn)); + QVERIFY(clientStream->sendHEADERS(getRequiredHeaders(), false)); + + // Server receives stream + QSignalSpy newStreamSpy{ serverConn, &QHttp2Connection::newIncomingStream }; + QVERIFY(newStreamSpy.wait()); + auto *serverStream = newStreamSpy.front().front().value<QHttp2Stream *>(); + QVERIFY(serverStream); + + QSignalSpy clientGoawaySpy{ clientConn, &QHttp2Connection::receivedGOAWAY }; + QSignalSpy clientClosedSpy{ clientConn, &QHttp2Connection::connectionClosed }; + QSignalSpy serverGoawaySpy{ serverConn, &QHttp2Connection::receivedGOAWAY }; + QSignalSpy serverClosedSpy{ serverConn, &QHttp2Connection::connectionClosed }; + QSignalSpy clientErrorSpy{ clientStream, &QHttp2Stream::errorOccurred }; + + serverConn->close(); // Server initiates graceful shutdown + + // Client receives initial GOAWAY with 2^31-1 + QVERIFY(clientGoawaySpy.wait()); + QCOMPARE(clientGoawaySpy.count(), 1); + QCOMPARE(clientGoawaySpy.at(0).at(0).value<Http2::Http2Error>(), Http2::HTTP2_NO_ERROR); + QCOMPARE(clientGoawaySpy.at(0).at(1).value<quint32>(), Http2::lastValidStreamID); + + // After receiving a GOAWAY we should not be able to create new streams + auto clientStream2 = clientConn->createStream(); + QCOMPARE_EQ(clientStream2.error(), QHttp2Connection::CreateStreamError::ReceivedGOAWAY); + + // Client receives final GOAWAY with actual lastStreamID (after PING RTT) + QVERIFY(clientGoawaySpy.wait(500)); + QCOMPARE(clientGoawaySpy.count(), 2); + QCOMPARE(clientGoawaySpy.at(1).at(0).value<Http2::Http2Error>(), Http2::HTTP2_NO_ERROR); + const quint32 finalLastStreamId = clientGoawaySpy.at(1).at(1).value<quint32>(); + QCOMPARE(finalLastStreamId, clientStream->streamID()); + + // Stream still active - graceful shutdown allows completion + QVERIFY(clientStream->isActive()); + QCOMPARE(clientClosedSpy.count(), 0); + + if (scenario == "increasing-lastStreamId"_L1) { + QLatin1StringView errMsg("Repeated GOAWAY with invalid last stream ID"); + QTest::ignoreMessage(QtCriticalMsg, QRegularExpression(".*" + errMsg + ".*")); + + // Send GOAWAY with higher lastStreamId than the final one (protocol violation) + const quint32 invalidHigherId = finalLastStreamId + 2; + serverConn->sendGOAWAYFrame(Http2::HTTP2_NO_ERROR, invalidHigherId); + + QTRY_COMPARE(clientErrorSpy.count(), 1); + QCOMPARE(clientErrorSpy.count(), 1); + QCOMPARE(clientErrorSpy.first().first().value<Http2::Http2Error>(), Http2::PROTOCOL_ERROR); + QCOMPARE(clientErrorSpy.first().last().value<QString>(), errMsg); + + // Client detects violation and responds with PROTOCOL_ERROR + QVERIFY(serverGoawaySpy.wait()); + QCOMPARE(serverGoawaySpy.last().at(0).value<Http2::Http2Error>(), Http2::PROTOCOL_ERROR); + + QTRY_COMPARE(clientClosedSpy.count(), 1); + QTRY_COMPARE(serverClosedSpy.count(), 1); + return; + } else if (scenario == "graceful-then-error") { + // RFC 9113 6.8: An endpoint MAY send multiple GOAWAY frames if circumstances change + serverConn->close(Http2::INTERNAL_ERROR); + + // Client receives error GOAWAY + QTRY_COMPARE(clientGoawaySpy.count(), 3); + QCOMPARE(clientGoawaySpy.at(2).at(0).value<Http2::Http2Error>(), Http2::INTERNAL_ERROR); + + // Error GOAWAY lastStreamId must not exceed previous + QVERIFY(clientGoawaySpy.at(2).at(1).value<quint32>() <= finalLastStreamId); + + // Server closes immediately after error + QTRY_COMPARE(serverClosedSpy.count(), 1); + + // Client stream should receive error + QVERIFY(!clientStream->isActive()); + QTRY_COMPARE(clientErrorSpy.count(), 1); + QCOMPARE(clientErrorSpy.count(), 1); + QCOMPARE(clientErrorSpy.first().first().value<Http2::Http2Error>(), Http2::INTERNAL_ERROR); + QTRY_COMPARE(clientClosedSpy.count(), 1); + + // Additional close() calls should be ignored + serverConn->close(); + serverConn->close(Http2::FLOW_CONTROL_ERROR); + qApp->processEvents(); + QCOMPARE(clientGoawaySpy.count(), 3); // No new GOAWAY + return; + } else if ("graceful-shutdown") { + QSignalSpy serverDataSpy{ serverStream, &QHttp2Stream::dataReceived }; + QVERIFY(clientStream->sendDATA("final-data", true)); + QVERIFY(serverDataSpy.wait()); + + QSignalSpy clientHeadersSpy{ clientStream, &QHttp2Stream::headersReceived }; + const HPack::HttpHeader responseHeaders{ { ":status", "200" } }; + QVERIFY(serverStream->sendHEADERS(responseHeaders, true)); + QVERIFY(clientHeadersSpy.wait()); + + QCOMPARE(clientStream->state(), QHttp2Stream::State::Closed); + + // Connection closes after all streams complete + QTRY_COMPARE(serverClosedSpy.count(), 1); + QTRY_COMPARE(clientClosedSpy.count(), 1); + + // No additional GOAWAYs + QCOMPARE(clientGoawaySpy.count(), 2); + } +} + +void tst_QHttp2Connection::clientInitiatedGoaway() +{ + // Clients don't need two-phase GOAWAY because they control their own + // stream creation (no race condition). Client sends single GOAWAY with the + // last server-initiated (even) stream ID it processed. + auto [client, server] = makeFakeConnectedSockets(); + auto clientConn = makeHttp2Connection(client.get(), {}, Client); + auto serverConn = makeHttp2Connection(server.get(), {}, Server); + + // Client creates stream + auto *clientStream = clientConn->createStream().unwrap(); + QVERIFY(clientStream); + QVERIFY(waitForSettingsExchange(clientConn, serverConn)); + QVERIFY(clientStream->sendHEADERS(getRequiredHeaders(), false)); + + // Server receives stream + QSignalSpy newStreamSpy{ serverConn, &QHttp2Connection::newIncomingStream }; + QVERIFY(newStreamSpy.wait()); + auto *serverStream = newStreamSpy.front().front().value<QHttp2Stream *>(); + QVERIFY(serverStream); + + QSignalSpy clientGoawaySpy{ clientConn, &QHttp2Connection::receivedGOAWAY }; + QSignalSpy clientClosedSpy{ clientConn, &QHttp2Connection::connectionClosed }; + QSignalSpy serverGoawaySpy{ serverConn, &QHttp2Connection::receivedGOAWAY }; + QSignalSpy serverClosedSpy{ serverConn, &QHttp2Connection::connectionClosed }; + + // Client initiates graceful shutdown + clientConn->close(); + + // Client should not be able to create new streams now + auto rejectedClientStream = clientConn->createStream(); + QCOMPARE_EQ(rejectedClientStream.error(), QHttp2Connection::CreateStreamError::ReceivedGOAWAY); + + // Server receives GOAWAY + QVERIFY(serverGoawaySpy.wait()); + QCOMPARE(serverGoawaySpy.count(), 1); + QCOMPARE(serverGoawaySpy.at(0).at(0).value<Http2::Http2Error>(), Http2::HTTP2_NO_ERROR); + + const quint32 lastStreamId = serverGoawaySpy.at(0).at(1).value<quint32>(); + QCOMPARE(lastStreamId, 0u); + + // Existing streams can still complete + QVERIFY(clientStream->isActive()); + QVERIFY(serverStream->isActive()); + + // Complete the stream exchange + QSignalSpy serverDataSpy{ serverStream, &QHttp2Stream::dataReceived }; + QVERIFY(clientStream->sendDATA("final-data", true)); + QVERIFY(serverDataSpy.wait()); + + QSignalSpy clientHeadersSpy{ clientStream, &QHttp2Stream::headersReceived }; + const HPack::HttpHeader responseHeaders{ { ":status", "200" } }; + QVERIFY(serverStream->sendHEADERS(responseHeaders, true)); + QVERIFY(clientHeadersSpy.wait()); + + QCOMPARE(clientStream->state(), QHttp2Stream::State::Closed); + QCOMPARE(serverStream->state(), QHttp2Stream::State::Closed); + + QTRY_COMPARE(clientClosedSpy.count(), 1); + + QCOMPARE(serverGoawaySpy.count(), 1); + QTRY_COMPARE(serverClosedSpy.count(), 1); +} + QTEST_MAIN(tst_QHttp2Connection) #include "tst_qhttp2connection.moc" diff --git a/tests/auto/widgets/widgets/qtabbar/tst_qtabbar.cpp b/tests/auto/widgets/widgets/qtabbar/tst_qtabbar.cpp index 16a69e4337d..4de2b255dd5 100644 --- a/tests/auto/widgets/widgets/qtabbar/tst_qtabbar.cpp +++ b/tests/auto/widgets/widgets/qtabbar/tst_qtabbar.cpp @@ -1100,12 +1100,14 @@ void tst_QTabBar::kineticWheel() leftEdge = QPoint(0, 0); rightEdge = leftButton->geometry().topLeft(); } - // avoid border lines - leftEdge += QPoint(2, 2); + // make sure the point is inside tabbar rect + const auto tabbarCenter = tabbar.geometry().center(); if (horizontal) { - rightEdge += QPoint(-2, 2); + leftEdge = QPoint(leftEdge.x() + 10, tabbarCenter.y()); + rightEdge = QPoint(rightEdge.x() - 10, tabbarCenter.y()); } else { - rightEdge += QPoint(2, -2); + leftEdge = QPoint(tabbarCenter.x(), leftEdge.y() + 10); + rightEdge = QPoint(tabbarCenter.x(), rightEdge.y() - 10); } QCOMPARE(tabbar.tabAt(leftEdge), 0); @@ -1393,7 +1395,7 @@ void tst_QTabBar::hoverTab() QCOMPARE(tabbar.styleOptions[2].state & QStyle::State_MouseOver, QStyle::State_None); // inserting a tab at index 2 again should paint the new tab hovered - tabbar.insertTab(2, "C2"); + tabbar.insertTab(2, "X"); QTRY_COMPARE(tabbar.styleOptions[2].state & QStyle::State_MouseOver, QStyle::State_MouseOver); QCOMPARE(tabbar.styleOptions[1].state & QStyle::State_MouseOver, QStyle::State_None); } |
