From 778371b8ea4fd34f1ee09e9206c3b42a99be7e1a Mon Sep 17 00:00:00 2001 From: Dennis Oberst Date: Thu, 24 Apr 2025 16:14:59 +0200 Subject: [PATCH] Deprecate QHash metadata in favor of QMultiHash Even though gRPC provides some leeway, as "Access to metadata is language dependent", we have yet to see an implementation which doesn't support multiple values per key. Common implementations like grpc-c++, grpc-go or grpc-java provide this and we should do it aswell. Ref: https://grpc.io/docs/what-is-grpc/core-concepts/#metadata Ref: https://github.com/grpc/grpc-go/blob/master/Documentation/grpc-metadata.md#constructing-metadata Use a base class to implement common functionality for the options classes. Also use a new common class in the tests. The QMultiHash overload is selected when the 'QtGrpc::MultiValue' argument is used in 'metadata' calls. We update the documentation accordingly. Deprecation is scheduled for Qt 6.13 Users with a custom Qt build - those who care - should be rewarded with minimal traces of this "accident" and should not suffer from potential performance/storage overhead. Therefore we deprecate QtGrpc::MultiValue for those builds, effectively providing Qt 7 behavior. [ChangeLog][Deprecation Notice] Deprecate the metadata()/setMetadata() methods on QGrpcCallOptions and QGrpcChannelOptions that use QHash in favor of the new overloads that use QMultiHash. This is more in line with the gRPC specification. Fixes: QTBUG-136471 Change-Id: I58d14d2c304c06de269c99ba5383beee86d12f77 Reviewed-by: Alexey Edelev --- src/grpc/CMakeLists.txt | 1 + src/grpc/qgrpccalloptions.cpp | 178 +++++++++---- src/grpc/qgrpccalloptions.h | 16 ++ src/grpc/qgrpcchanneloptions.cpp | 184 ++++++++++---- src/grpc/qgrpcchanneloptions.h | 18 +- src/grpc/qgrpccommonoptions.cpp | 140 +++++++++++ src/grpc/qgrpccommonoptions_p.h | 80 ++++++ src/grpc/qgrpchttp2channel.cpp | 8 +- src/grpc/qtgrpcnamespace.h | 8 + src/grpc/qtgrpcnamespace.qdoc | 18 ++ src/grpcquick/qqmlgrpcmetadata_p.h | 4 +- .../auto/grpc/qgrpccalloptions/CMakeLists.txt | 2 + .../qgrpccalloptions/tst_qgrpccalloptions.cpp | 127 ++-------- .../grpc/qgrpcchanneloptions/CMakeLists.txt | 2 + .../tst_qgrpcchanneloptions.cpp | 129 ++-------- tests/auto/grpc/shared/grpccommonoptions.h | 237 ++++++++++++++++++ 16 files changed, 819 insertions(+), 333 deletions(-) create mode 100644 src/grpc/qgrpccommonoptions.cpp create mode 100644 src/grpc/qgrpccommonoptions_p.h create mode 100644 tests/auto/grpc/shared/grpccommonoptions.h diff --git a/src/grpc/CMakeLists.txt b/src/grpc/CMakeLists.txt index e39c01ce..beffeefc 100644 --- a/src/grpc/CMakeLists.txt +++ b/src/grpc/CMakeLists.txt @@ -11,6 +11,7 @@ qt_internal_add_module(Grpc qabstractgrpcchannel.h qabstractgrpcchannel_p.h qabstractgrpcchannel.cpp qgrpchttp2channel.h qgrpchttp2channel.cpp qgrpcclientbase.h qgrpcclientbase.cpp + qgrpccommonoptions_p.h qgrpccommonoptions.cpp qgrpccalloptions.h qgrpccalloptions.cpp qgrpcchanneloptions.h qgrpcchanneloptions.cpp qtgrpcglobal.h diff --git a/src/grpc/qgrpccalloptions.cpp b/src/grpc/qgrpccalloptions.cpp index a75c83cc..663b7704 100644 --- a/src/grpc/qgrpccalloptions.cpp +++ b/src/grpc/qgrpccalloptions.cpp @@ -1,6 +1,8 @@ // Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +#include +#include #include #include @@ -23,13 +25,27 @@ using namespace Qt::StringLiterals; These options supersede the ones set via QGrpcChannelOptions. To configure the default options shared by RPCs, use QGrpcChannelOptions. + + \code + QGrpcCallOptions callOpts; + // Set the metadata for an individial RPC + callOpts.setMetadata({ + { "header" , "value1" }, + { "header" , "value2" }, + }); + const auto &md = callOpts.metadata(QtGrpc::MultiValue); + qDebug() << "Call Metadata: " << md; + + // Set a 2-second deadline for an individial RPC + callOpts.setDeadlineTimeout(2s); + qDebug() << "Call timeout: " << callOpts.deadlineTimeout(); + \endcode */ -class QGrpcCallOptionsPrivate : public QSharedData +class QGrpcCallOptionsPrivate : public QGrpcCommonOptions { public: - std::optional timeout; - QHash metadata; + QGrpcCallOptionsPrivate() = default; }; QT_DEFINE_QESDP_SPECIALIZATION_DTOR(QGrpcCallOptionsPrivate) @@ -92,98 +108,152 @@ QGrpcCallOptions::operator QVariant() const */ /*! - Sets the \a timeout for a specific RPC and returns a reference to the - updated object. + \include qgrpccommonoptions.cpp set-deadline-timeout -//! [set-deadline-desc] - A deadline sets the limit for how long a client is willing to wait for a - response from a server. The actual deadline is computed by adding the \a - timeout to the start time of the RPC. + \note Setting this field \b{overrides} the corresponding channel options field + — see \l{QGrpcChannelOptions::setDeadlineTimeout()} - The deadline applies to the entire lifetime of an RPC, which includes - receiving the final QGrpcStatus for a previously started call and can thus - be unwanted for (long-lived) streams. -//! [set-deadline-desc] - - \note Setting this field overrides the value set by - QGrpcChannelOptions::setDeadline() for a specific RPC. + \sa deadlineTimeout() */ QGrpcCallOptions &QGrpcCallOptions::setDeadlineTimeout(std::chrono::milliseconds timeout) { - if (d_ptr->timeout == timeout) + if (d_ptr->deadlineTimeout() == timeout) return *this; d_ptr.detach(); Q_D(QGrpcCallOptions); - d->timeout = timeout; + d->setDeadlineTimeout(timeout); return *this; } +/*! + \include qgrpccommonoptions.cpp deadline-timeout +*/ +std::optional QGrpcCallOptions::deadlineTimeout() const noexcept +{ + Q_D(const QGrpcCallOptions); + return d->deadlineTimeout(); +} + +#if QT_DEPRECATED_SINCE(6, 13) + +/*! + \fn const QHash &QGrpcCallOptions::metadata() const & + \fn QHash QGrpcCallOptions::metadata() && + \deprecated [6.13] Use the QMultiHash overload instead. + + \include qgrpccommonoptions.cpp metadata + + \sa setMetadata() +*/ +const QHash &QGrpcCallOptions::metadata() const & noexcept +{ + Q_D(const QGrpcCallOptions); + return d->metadata(); +} +QHash QGrpcCallOptions::metadata() && +{ + Q_D(QGrpcCallOptions); + if (d->ref.loadRelaxed() != 1) + return d->metadata(); + return std::move(*d_ptr).metadata(); +} + /*! \fn QGrpcCallOptions &QGrpcCallOptions::setMetadata(const QHash &metadata) \fn QGrpcCallOptions &QGrpcCallOptions::setMetadata(QHash &&metadata) + \deprecated [6.13] Use the QMultiHash overload instead. - Sets the client \a metadata for a specific RPC and returns a reference to the - updated object. + \include qgrpccommonoptions.cpp set-metadata -//! [set-metadata-desc] - QGrpcHttp2Channel converts the metadata into appropriate HTTP/2 headers - which will be added to the HTTP/2 request. -//! [set-metadata-desc] +//! [merge-md-note] + \note Call metadata is \b{merged} with any channel-level metadata when the + RPC starts — see +//! [merge-md-note] + \l{QGrpcChannelOptions::setMetadata(const QMultiHash&)}{QGrpcChannelOptions::setMetadata(QMultiHash)}. - \note Setting this field overrides the value set by - QGrpcChannelOptions::setMetadata() for a specific RPC. + \sa metadata() */ QGrpcCallOptions &QGrpcCallOptions::setMetadata(const QHash &metadata) { - if (d_ptr->metadata == metadata) + if (d_ptr->metadata(QtGrpc::MultiValue) == metadata) return *this; d_ptr.detach(); Q_D(QGrpcCallOptions); - d->metadata = metadata; + d->setMetadata(metadata); return *this; } - QGrpcCallOptions &QGrpcCallOptions::setMetadata(QHash &&metadata) { - if (d_ptr->metadata == metadata) + if (d_ptr->metadata(QtGrpc::MultiValue) == metadata) return *this; d_ptr.detach(); Q_D(QGrpcCallOptions); - d->metadata = std::move(metadata); + d->setMetadata(std::move(metadata)); return *this; } -/*! - Returns the timeout duration that is used to calculate the deadline for a - specific RPC. - - If this field is unset, returns an empty \c {std::optional}. -*/ -std::optional QGrpcCallOptions::deadlineTimeout() const noexcept -{ - Q_D(const QGrpcCallOptions); - return d->timeout; -} +#endif // QT_DEPRECATED_SINCE(6, 13) /*! - \fn const QHash &QGrpcCallOptions::metadata() const & - \fn QHash QGrpcCallOptions::metadata() && + \since 6.10 + \fn const QMultiHash &QGrpcCallOptions::metadata(QtGrpc::MultiValueTag) const & + \fn QMultiHash QGrpcCallOptions::metadata(QtGrpc::MultiValueTag) && - Returns the client metadata for a specific RPC. - If this field is unset, returns empty metadata. + \include qgrpccommonoptions.cpp metadata-multi + + \sa {setMetadata(const QMultiHash&)}{setMetadata} */ -const QHash &QGrpcCallOptions::metadata() const & noexcept +const QMultiHash & +QGrpcCallOptions::metadata(QtGrpc::MultiValueTag tag) const & noexcept { Q_D(const QGrpcCallOptions); - return d->metadata; + return d->metadata(tag); } - -QHash QGrpcCallOptions::metadata() && +QMultiHash QGrpcCallOptions::metadata(QtGrpc::MultiValueTag tag) && { Q_D(QGrpcCallOptions); - if (d->ref.loadRelaxed() != 1) // return copy if shared - return { d->metadata }; - return std::move(d->metadata); + if (d->ref.loadRelaxed() != 1) + return d->metadata(tag); + return std::move(*d_ptr).metadata(tag); +} + +/*! + \since 6.10 + \fn QGrpcCallOptions &QGrpcCallOptions::setMetadata(const QMultiHash &metadata) + \fn QGrpcCallOptions &QGrpcCallOptions::setMetadata(QMultiHash &&metadata) + \fn QGrpcCallOptions &QGrpcCallOptions::setMetadata(std::initializer_list> metadata) + + \include qgrpccommonoptions.cpp set-metadata-multi + + \include qgrpccalloptions.cpp merge-md-note + \l{QGrpcChannelOptions::setMetadata(const QMultiHash&)}{QGrpcChannelOptions::setMetadata(QMultiHash)}. + + \sa metadata(QtGrpc::MultiValueTag) +*/ +QGrpcCallOptions &QGrpcCallOptions::setMetadata(const QMultiHash &metadata) +{ + if (d_ptr->metadata(QtGrpc::MultiValue) == metadata) + return *this; + d_ptr.detach(); + Q_D(QGrpcCallOptions); + d->setMetadata(metadata); + return *this; +} +QGrpcCallOptions &QGrpcCallOptions::setMetadata(QMultiHash &&metadata) +{ + if (d_ptr->metadata(QtGrpc::MultiValue) == metadata) + return *this; + d_ptr.detach(); + Q_D(QGrpcCallOptions); + d->setMetadata(std::move(metadata)); + return *this; +} +QGrpcCallOptions & +QGrpcCallOptions::setMetadata(std::initializer_list> list) +{ + return setMetadata(QMultiHash(list)); } #ifndef QT_NO_DEBUG_STREAM @@ -198,7 +268,7 @@ QDebug operator<<(QDebug debug, const QGrpcCallOptions &callOpts) const QDebugStateSaver save(debug); debug.nospace().noquote(); debug << "QGrpcCallOptions(deadline: " << callOpts.deadlineTimeout() - << ", metadata: " << callOpts.metadata() << ')'; + << ", metadata: " << callOpts.metadata(QtGrpc::MultiValue) << ')'; return debug; } #endif diff --git a/src/grpc/qgrpccalloptions.h b/src/grpc/qgrpccalloptions.h index 3eb4b4d9..98a09d68 100644 --- a/src/grpc/qgrpccalloptions.h +++ b/src/grpc/qgrpccalloptions.h @@ -5,11 +5,13 @@ #define QGRPCALLOPTIONS_H #include +#include #include #include #include #include +#include #include #include @@ -42,10 +44,24 @@ public: deadlineTimeout() const noexcept; Q_GRPC_EXPORT QGrpcCallOptions &setDeadlineTimeout(std::chrono::milliseconds timeout); +#if QT_DEPRECATED_SINCE(6, 13) + QT_DEPRECATED_VERSION_X_6_13("Use metadata(QtGrpc::MultiValue) for QMultiHash") [[nodiscard]] Q_GRPC_EXPORT const QHash &metadata() const & noexcept; + QT_DEPRECATED_VERSION_X_6_13("Use metadata(QtGrpc::MultiValue) for QMultiHash") [[nodiscard]] Q_GRPC_EXPORT QHash metadata() &&; + QT_DEPRECATED_VERSION_X_6_13("Use the QMultiHash overload") Q_GRPC_EXPORT QGrpcCallOptions &setMetadata(const QHash &metadata); + QT_DEPRECATED_VERSION_X_6_13("Use the QMultiHash overload") Q_GRPC_EXPORT QGrpcCallOptions &setMetadata(QHash &&metadata); +#endif + [[nodiscard]] Q_GRPC_EXPORT const QMultiHash & + metadata(QtGrpc::MultiValueTag) const & noexcept; + [[nodiscard]] Q_GRPC_EXPORT QMultiHash + metadata(QtGrpc::MultiValueTag) &&; + Q_GRPC_EXPORT QGrpcCallOptions &setMetadata(const QMultiHash &metadata); + Q_GRPC_EXPORT QGrpcCallOptions &setMetadata(QMultiHash &&metadata); + Q_GRPC_EXPORT QGrpcCallOptions & + setMetadata(std::initializer_list> list); private: QExplicitlySharedDataPointer d_ptr; diff --git a/src/grpc/qgrpcchanneloptions.cpp b/src/grpc/qgrpcchanneloptions.cpp index 37959b4b..2ec8c7da 100644 --- a/src/grpc/qgrpcchanneloptions.cpp +++ b/src/grpc/qgrpcchanneloptions.cpp @@ -1,6 +1,7 @@ // Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +#include #include #include #include @@ -25,17 +26,34 @@ using namespace QtGrpc; to all remote procedure calls (RPCs) that operate on the associated channel, which is used to communicate with services. - Override options for specific RPCs with QGrcCallOptions. + Override options for specific RPCs with QGrpcCallOptions. + + \code + QGrpcChannelOptions channelOpts; + // Apply common metadata to every RPC + channelOpts.setMetadata({ + { "header" , "value1" }, + { "header" , "value2" }, + }); + const auto &md = channelOpts.metadata(QtGrpc::MultiValue); + qDebug() << "Channel Metadata: " << md; + + // Apply a 2-second deadline to every RPC + channelOpts.setDeadlineTimeout(2s); + qDebug() << "Channel timeout: " << channelOpts.deadlineTimeout(); + + // Configure SSL/TLS configuration + channelOpts.setSslConfiguration(QSslConfiguration()); + \endcode + \note It is up to the channel's implementation to determine the specifics of these options. */ -class QGrpcChannelOptionsPrivate : public QSharedData +class QGrpcChannelOptionsPrivate : public QGrpcCommonOptions { public: - std::optional timeout; - QHash metadata; QGrpcSerializationFormat serializationFormat; #if QT_CONFIG(ssl) std::optional sslConfiguration; @@ -101,57 +119,151 @@ QGrpcChannelOptions::operator QVariant() const } /*! - Sets the \a timeout for the channel and returns a reference to the updated - object. + \include qgrpccommonoptions.cpp set-deadline-timeout - \include qgrpccalloptions.cpp set-deadline-desc +//! [channel-note] + \note Setting this field applies to all RPCs that operate on the channel, + except those overriden by +//! [channel-note] + \l{QGrpcCallOptions::setDeadlineTimeout()} - \note The deadline set via the channel options applies to all RPCs that - operate on the channel, except those overridden by - QGrpcCallOptions::setDeadline(). + \sa deadlineTimeout() */ QGrpcChannelOptions &QGrpcChannelOptions::setDeadlineTimeout(std::chrono::milliseconds timeout) { - if (d_ptr->timeout == timeout) + if (d_ptr->deadlineTimeout() == timeout) return *this; d_ptr.detach(); Q_D(QGrpcChannelOptions); - d->timeout = timeout; + d->setDeadlineTimeout(timeout); return *this; } +#if QT_DEPRECATED_SINCE(6, 13) + +/*! + \fn const QHash &QGrpcChannelOptions::metadata() const & + \fn QHash QGrpcChannelOptions::metadata() && + \deprecated [6.13] Use \l{metadata(QtGrpc::MultiValueTag)}{metadata(QtGrpc::MultiValue)} instead. + + \include qgrpccommonoptions.cpp metadata + + \sa metadata(QtGrpc::MultiValueTag), setMetadata() +*/ +const QHash &QGrpcChannelOptions::metadata() const & noexcept +{ + Q_D(const QGrpcChannelOptions); + return d->metadata(); +} +QHash QGrpcChannelOptions::metadata() && +{ + Q_D(QGrpcChannelOptions); + if (d->ref.loadRelaxed() != 1) // return copy if shared + return d->metadata(); + return std::move(*d_ptr).metadata(); +} + /*! \fn QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(const QHash &metadata) \fn QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(QHash &&metadata) + \deprecated [6.13] Use the QMultiHash overload instead. - Sets the client \a metadata for the channel and returns a reference to the - updated object. + \include qgrpccommonoptions.cpp set-metadata - \include qgrpccalloptions.cpp set-metadata-desc +//! [merge-md-note] + \note This metadata is included in every RPC made through the channel. + Channel metadata is \b{merged} with any call-specific metadata when the RPC + starts — see +//! [merge-md-note] + \l{QGrpcCallOptions::setMetadata(const QMultiHash&)}{QGrpcCallOptions::setMetadata(QMultiHash)} - \note The metadata set via the channel options applies to all RPCs that - operate on the channel, except those overridden by - QGrpcCallOptions::setMetadata(). + \sa metadata() */ QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(const QHash &metadata) { - if (d_ptr->metadata == metadata) + if (d_ptr->metadata(QtGrpc::MultiValue) == metadata) return *this; d_ptr.detach(); Q_D(QGrpcChannelOptions); - d->metadata = metadata; + d->setMetadata(metadata); + return *this; +} +QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(QHash &&metadata) +{ + if (d_ptr->metadata(QtGrpc::MultiValue) == metadata) + return *this; + d_ptr.detach(); + Q_D(QGrpcChannelOptions); + d->setMetadata(std::move(metadata)); return *this; } -QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(QHash &&metadata) +#endif // QT_DEPRECATED_SINCE(6, 13) + +/*! + \since 6.10 + \fn const QMultiHash &QGrpcChannelOptions::metadata(QtGrpc::MultiValueTag) const & + \fn QMultiHash QGrpcChannelOptions::metadata(QtGrpc::MultiValueTag) && + + \include qgrpccommonoptions.cpp metadata-multi + + \sa {setMetadata(const QMultiHash&)}{setMetadata} +*/ +const QMultiHash & +QGrpcChannelOptions::metadata(QtGrpc::MultiValueTag tag) const & noexcept { - if (d_ptr->metadata == metadata) + Q_D(const QGrpcChannelOptions); + return d->metadata(tag); +} + +QMultiHash +QGrpcChannelOptions::metadata(QtGrpc::MultiValueTag tag) && +{ + Q_D(QGrpcChannelOptions); + if (d->ref.loadRelaxed() != 1) + return d->metadata(tag); + return std::move(*d_ptr).metadata(tag); +} + +/*! + \since 6.10 + \fn QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(const QMultiHash &metadata) + \fn QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(QMultiHash &&metadata) + \fn QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(std::initializer_list> list) + + \include qgrpccommonoptions.cpp set-metadata-multi + + \include qgrpcchanneloptions.cpp merge-md-note + \l{QGrpcCallOptions::setMetadata(const QMultiHash&)}{QGrpcCallOptions::setMetadata(QMultiHash)} + + \sa metadata(QtGrpc::MultiValueTag) +*/ +QGrpcChannelOptions & +QGrpcChannelOptions::setMetadata(const QMultiHash &metadata) +{ + if (d_ptr->metadata(QtGrpc::MultiValue) == metadata) return *this; d_ptr.detach(); Q_D(QGrpcChannelOptions); - d->metadata = std::move(metadata); + d->setMetadata(metadata); return *this; } +QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(QMultiHash &&metadata) +{ + if (d_ptr->metadata(QtGrpc::MultiValue) == metadata) + return *this; + d_ptr.detach(); + Q_D(QGrpcChannelOptions); + d->setMetadata(std::move(metadata)); + return *this; +} +QGrpcChannelOptions & +QGrpcChannelOptions::setMetadata(std::initializer_list> list) +{ + return setMetadata(QMultiHash(list)); +} /*! \since 6.8 @@ -179,29 +291,7 @@ QGrpcChannelOptions::setSerializationFormat(const QGrpcSerializationFormat &form std::optional QGrpcChannelOptions::deadlineTimeout() const noexcept { Q_D(const QGrpcChannelOptions); - return d->timeout; -} - -/*! - \fn const QHash &QGrpcChannelOptions::metadata() const & - \fn QHash QGrpcChannelOptions::metadata() && - - Returns the client metadata for the channel. - - If this field is unset, returns empty metadata. -*/ -const QHash &QGrpcChannelOptions::metadata() const & noexcept -{ - Q_D(const QGrpcChannelOptions); - return d->metadata; -} - -QHash QGrpcChannelOptions::metadata() && -{ - Q_D(QGrpcChannelOptions); - if (d->ref.loadRelaxed() != 1) // return copy if shared - return { d->metadata }; - return std::move(d->metadata); + return d->deadlineTimeout(); } /*! @@ -258,7 +348,7 @@ QDebug operator<<(QDebug debug, const QGrpcChannelOptions &chOpts) const QDebugStateSaver save(debug); debug.nospace().noquote(); debug << "QGrpcChannelOptions(deadline: " << chOpts.deadlineTimeout() - << ", metadata: " << chOpts.metadata() + << ", metadata: " << chOpts.metadata(QtGrpc::MultiValue) << ", serializationFormat: " << chOpts.serializationFormat().suffix() << ", sslConfiguration: "; # if QT_CONFIG(ssl) diff --git a/src/grpc/qgrpcchanneloptions.h b/src/grpc/qgrpcchanneloptions.h index f8e1a8db..a6896dd2 100644 --- a/src/grpc/qgrpcchanneloptions.h +++ b/src/grpc/qgrpcchanneloptions.h @@ -5,6 +5,7 @@ #define QGRPCHANNELOPTIONS_H #include +#include #if QT_CONFIG(ssl) # include @@ -14,7 +15,7 @@ #include #include #include -#include +#include #include #include @@ -48,10 +49,25 @@ public: deadlineTimeout() const noexcept; Q_GRPC_EXPORT QGrpcChannelOptions &setDeadlineTimeout(std::chrono::milliseconds timeout); +#if QT_DEPRECATED_SINCE(6, 13) + QT_DEPRECATED_VERSION_X_6_13("Use metadata(QtGrpc::MultiValue) for QMultiHash") [[nodiscard]] Q_GRPC_EXPORT const QHash &metadata() const & noexcept; + QT_DEPRECATED_VERSION_X_6_13("Use metadata(QtGrpc::MultiValue) for QMultiHash") [[nodiscard]] Q_GRPC_EXPORT QHash metadata() &&; + QT_DEPRECATED_VERSION_X_6_13("Use the QMultiHash overload") Q_GRPC_EXPORT QGrpcChannelOptions &setMetadata(const QHash &metadata); + QT_DEPRECATED_VERSION_X_6_13("Use the QMultiHash overload") Q_GRPC_EXPORT QGrpcChannelOptions &setMetadata(QHash &&metadata); +#endif + [[nodiscard]] Q_GRPC_EXPORT const QMultiHash & + metadata(QtGrpc::MultiValueTag) const & noexcept; + [[nodiscard]] Q_GRPC_EXPORT QMultiHash + metadata(QtGrpc::MultiValueTag) &&; + Q_GRPC_EXPORT QGrpcChannelOptions & + setMetadata(const QMultiHash &metadata); + Q_GRPC_EXPORT QGrpcChannelOptions &setMetadata(QMultiHash &&metadata); + Q_GRPC_EXPORT QGrpcChannelOptions & + setMetadata(std::initializer_list> list); [[nodiscard]] Q_GRPC_EXPORT QGrpcSerializationFormat serializationFormat() const; Q_GRPC_EXPORT QGrpcChannelOptions & diff --git a/src/grpc/qgrpccommonoptions.cpp b/src/grpc/qgrpccommonoptions.cpp new file mode 100644 index 00000000..9f237af5 --- /dev/null +++ b/src/grpc/qgrpccommonoptions.cpp @@ -0,0 +1,140 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include + +QT_BEGIN_NAMESPACE + +#if QT_DEPRECATED_SINCE(6, 13) + +namespace +{ + +inline QHash mergeHash(const QMultiHash &multiHash) +{ + QHash out; + for (const auto &key : multiHash.uniqueKeys()) + out.insert(key, multiHash.value(key)); + return out; +} + +} // namespace + +/*! +//! [metadata] + Returns the metadata. If this field is unset, returns empty + metadata. +//! [metadata] +*/ +const QHash &QGrpcCommonOptions::metadata() const & +{ + m_deprecatedQHashRefUsed = true; + if (m_metadataMulti != m_metadata) + m_metadata = mergeHash(m_metadataMulti); + return m_metadata; +} +QHash QGrpcCommonOptions::metadata() && +{ + if (m_metadataMulti != m_metadata) + m_metadata = mergeHash(m_metadataMulti); + return std::move(m_metadata); +} + +/*! +//! [set-metadata] + Sets the \a metadata and returns a reference to the updated object. + + When using QGrpcHttp2Channel, the metadata is converted to HTTP/2 headers + and added to the gRPC request. +//! [set-metadata] +*/ +void QGrpcCommonOptions::setMetadata(const QHash &md) +{ + if (m_deprecatedQHashRefUsed) + m_metadata = md; + m_metadataMulti = QMultiHash(md); +} +void QGrpcCommonOptions::setMetadata(QHash &&md) +{ + if (m_deprecatedQHashRefUsed) + m_metadata = md; + m_metadataMulti = QMultiHash(std::move(md)); +} + +#endif // QT_DEPRECATED_SINCE(6, 13) + +/*! +//! [deadline-timeout] + Returns the timeout duration that is used to calculate the deadline for RPCs. + + If this field is unset, returns an empty \c {std::optional}. +//! [deadline-timeout] +*/ +std::optional QGrpcCommonOptions::deadlineTimeout() const noexcept +{ + return m_timeout; +} + +/*! +//! [set-deadline-timeout] + Sets the \a timeout and returns a reference to the updated object. + + A deadline sets the limit for how long a client is willing to wait for a + response from a server. The actual deadline is computed by adding the \a + timeout to the start time of the RPC. + + The deadline applies to the entire lifetime of an RPC, which includes + receiving the final QGrpcStatus for a previously started call and can thus + be unwanted for (long-lived) streams. +//! [set-deadline-timeout] +*/ +void QGrpcCommonOptions::setDeadlineTimeout(std::chrono::milliseconds t) +{ + m_timeout = t; +} + +/*! +//! [metadata-multi] + \include qgrpccommonoptions.cpp metadata + Multiple values per key are supported. + + \code + const auto &md = opts.metadata(QtGrpc::MultiValue); + \endcode +//! [metadata-multi] +*/ +const QMultiHash & +QGrpcCommonOptions::metadata(QtGrpc::MultiValueTag /*tag*/) const & +{ + return m_metadataMulti; +} +QMultiHash QGrpcCommonOptions::metadata(QtGrpc::MultiValueTag /*tag*/) && +{ + return std::move(m_metadataMulti); +} + +/*! +//! [set-metadata-multi] + \include qgrpccommonoptions.cpp set-metadata + Multiple values per key are supported. +//! [set-metadata-multi] +*/ +void QGrpcCommonOptions::setMetadata(const QMultiHash &md) +{ + m_metadataMulti = md; +#if QT_DEPRECATED_SINCE(6, 13) + if (m_deprecatedQHashRefUsed) + m_metadata = mergeHash(m_metadataMulti); +#endif +} + +void QGrpcCommonOptions::setMetadata(QMultiHash &&md) +{ + m_metadataMulti = std::move(md); +#if QT_DEPRECATED_SINCE(6, 13) + if (m_deprecatedQHashRefUsed) + m_metadata = mergeHash(m_metadataMulti); +#endif +} + +QT_END_NAMESPACE diff --git a/src/grpc/qgrpccommonoptions_p.h b/src/grpc/qgrpccommonoptions_p.h new file mode 100644 index 00000000..85e2591a --- /dev/null +++ b/src/grpc/qgrpccommonoptions_p.h @@ -0,0 +1,80 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QTGRPCCOMMONOPTIONS_P_H +#define QTGRPCCOMMONOPTIONS_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include + +#include +#include +#include + +#include +#include + +QT_BEGIN_NAMESPACE + +class QGrpcCommonOptions : public QSharedData +{ +public: + QGrpcCommonOptions() = default; + virtual ~QGrpcCommonOptions() = default; + + [[nodiscard]] std::optional deadlineTimeout() const noexcept; + void setDeadlineTimeout(std::chrono::milliseconds t); + +#if QT_DEPRECATED_SINCE(6, 13) + const QHash &metadata() const &; + QHash metadata() &&; + void setMetadata(const QHash &md); + void setMetadata(QHash &&md); +#endif + const QMultiHash &metadata(QtGrpc::MultiValueTag /*tag*/) const &; + QMultiHash metadata(QtGrpc::MultiValueTag /*tag*/) &&; + void setMetadata(const QMultiHash &md); + void setMetadata(QMultiHash &&md); + +private: + std::optional m_timeout; + QMultiHash m_metadataMulti; +#if QT_DEPRECATED_SINCE(6, 13) + mutable QHash m_metadata; + mutable bool m_deprecatedQHashRefUsed = false; +#endif +}; + +inline bool operator==(const QMultiHash &multiHash, + const QHash &hash) +{ + if (hash.size() != multiHash.size()) + return false; + for (const auto &[k, v] : hash.asKeyValueRange()) { + const auto [f, l] = multiHash.equal_range(k); + if (f == l || std::next(f) != l || *f != v) + return false; + } + return true; +} + +inline bool operator!=(const QMultiHash &multiHash, + const QHash &hash) +{ + return !(multiHash == hash); +} + +QT_END_NAMESPACE + +#endif diff --git a/src/grpc/qgrpchttp2channel.cpp b/src/grpc/qgrpchttp2channel.cpp index abb9162a..4cc1a000 100644 --- a/src/grpc/qgrpchttp2channel.cpp +++ b/src/grpc/qgrpchttp2channel.cpp @@ -553,8 +553,8 @@ void Http2Handler::prepareInitialRequest(QGrpcOperationContext *operationContext } }; - iterateMetadata(channelOptions.metadata()); - iterateMetadata(operationContext->callOptions().metadata()); + iterateMetadata(channelOptions.metadata(QtGrpc::MultiValue)); + iterateMetadata(operationContext->callOptions().metadata(QtGrpc::MultiValue)); writeMessage(operationContext->argument()); } @@ -688,8 +688,8 @@ QGrpcHttp2ChannelPrivate::QGrpcHttp2ChannelPrivate(const QUrl &uri, QGrpcHttp2Ch : defaultContentType; bool warnAboutFormatConflict = !formatSuffix.isEmpty(); - const auto it = channelOptions.metadata().constFind(ContentTypeHeader.data()); - if (it != channelOptions.metadata().cend()) { + const auto it = channelOptions.metadata(QtGrpc::MultiValue).constFind(ContentTypeHeader.data()); + if (it != channelOptions.metadata(QtGrpc::MultiValue).cend()) { if (formatSuffix.isEmpty() && it.value() != DefaultContentType) { if (it.value() == "application/grpc+json") { channelOptions.setSerializationFormat(SerializationFormat::Json); diff --git a/src/grpc/qtgrpcnamespace.h b/src/grpc/qtgrpcnamespace.h index a4567cc2..225d21ef 100644 --- a/src/grpc/qtgrpcnamespace.h +++ b/src/grpc/qtgrpcnamespace.h @@ -47,6 +47,14 @@ enum class StatusCode : quint8 { }; Q_ENUM_NS(StatusCode) +// ### Qt7: remove QHash metadata interfaces. +enum class MultiValueTag : quint8 { Allow QT_DEPRECATED_X("use QtGrpc::MultiValue") }; +QT_WARNING_PUSH +QT_WARNING_DISABLE_DEPRECATED +QT_WARNING_DISABLE_CLANG("-Wunused-const-variable") +inline static constexpr auto MultiValue = QtGrpc::MultiValueTag::Allow; +QT_WARNING_POP + Q_CLASSINFO("RegisterEnumClassesUnscoped", "false") } // namespace QtGrpc diff --git a/src/grpc/qtgrpcnamespace.qdoc b/src/grpc/qtgrpcnamespace.qdoc index f277531e..93724c44 100644 --- a/src/grpc/qtgrpcnamespace.qdoc +++ b/src/grpc/qtgrpcnamespace.qdoc @@ -82,3 +82,21 @@ \sa{https://github.com/grpc/grpc/blob/master/doc/statuscodes.md}{gRPC status codes} */ +/*! + \since 6.10 + \enum QtGrpc::MultiValueTag + \brief Tag type used to access QMultiHash metadata. + \omitvalue Allow + \note This type is an implementation detail. Use QtGrpc::MultiValue for public API access. + + \sa QtGrpc::MultiValue, QGrpcChannelOptions::metadata(QtGrpc::MultiValueTag), + QGrpcCallOptions::metadata(QtGrpc::MultiValueTag) +*/ + +/*! + \since 6.10 + \variable QtGrpc::MultiValue + \brief Tag used to access QMultiHash metadata. + \sa QGrpcChannelOptions::metadata(QtGrpc::MultiValueTag), + QGrpcCallOptions::metadata(QtGrpc::MultiValueTag) +*/ diff --git a/src/grpcquick/qqmlgrpcmetadata_p.h b/src/grpcquick/qqmlgrpcmetadata_p.h index 3ca8f19e..5945fb99 100644 --- a/src/grpcquick/qqmlgrpcmetadata_p.h +++ b/src/grpcquick/qqmlgrpcmetadata_p.h @@ -37,7 +37,7 @@ public: explicit QQmlGrpcMetadata(QObject *parent = nullptr); ~QQmlGrpcMetadata() override; - const QHash &metadata() const & noexcept { return m_metadata; } + const QMultiHash &metadata() const & noexcept { return m_metadata; } void metadata() && = delete; const QVariantMap &data() const { return m_variantdata; } @@ -48,7 +48,7 @@ Q_SIGNALS: private: QVariantMap m_variantdata; - QHash m_metadata; + QMultiHash m_metadata; Q_DISABLE_COPY_MOVE(QQmlGrpcMetadata) }; diff --git a/tests/auto/grpc/qgrpccalloptions/CMakeLists.txt b/tests/auto/grpc/qgrpccalloptions/CMakeLists.txt index 222d84aa..21837c30 100644 --- a/tests/auto/grpc/qgrpccalloptions/CMakeLists.txt +++ b/tests/auto/grpc/qgrpccalloptions/CMakeLists.txt @@ -10,6 +10,8 @@ endif() qt_internal_add_test(tst_qgrpccalloptions SOURCES tst_qgrpccalloptions.cpp + INCLUDE_DIRECTORIES + "${CMAKE_CURRENT_LIST_DIR}/../shared" LIBRARIES Qt::Core Qt::Test diff --git a/tests/auto/grpc/qgrpccalloptions/tst_qgrpccalloptions.cpp b/tests/auto/grpc/qgrpccalloptions/tst_qgrpccalloptions.cpp index 74a509a4..626e217a 100644 --- a/tests/auto/grpc/qgrpccalloptions/tst_qgrpccalloptions.cpp +++ b/tests/auto/grpc/qgrpccalloptions/tst_qgrpccalloptions.cpp @@ -1,129 +1,32 @@ // Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +#include + #include #include -#include - -using namespace std::chrono_literals; - class QGrpcCallOptionsTest : public QObject { Q_OBJECT private Q_SLOTS: - void hasSpecialMemberFunctions() const; - void hasImplicitQVariant() const; - void hasMemberSwap() const; - void propertyMetadata() const; - void propertyDeadline() const; - void streamsToDebug() const; + void hasSpecialMemberFunctions() const { common.hasSpecialMemberFunctions(); } + void hasImplicitQVariant() const { common.hasImplicitQVariant(); } + void hasMemberSwap() const { common.hasMemberSwap(); } +#if QT_DEPRECATED_SINCE(6, 13) + void deprecatedPropertyMetadata() const { common.deprecatedPropertyMetadata(); } + void propertyMetadataCompat() const { common.propertyMetadataCompat(); } +#endif + void propertyMetadata() const { common.propertyMetadata(); } + void propertyDeadline() const { common.propertyDeadline(); } + void streamsToDebug() const { common.streamsToDebug(); } + +private: + GrpcCommonOptionsTest common; }; -void QGrpcCallOptionsTest::hasSpecialMemberFunctions() const -{ - QGrpcCallOptions o1; - QVERIFY(!o1.deadlineTimeout()); - QVERIFY(o1.metadata().empty()); - - o1.setDeadlineTimeout(100ms); - - QGrpcCallOptions o2(o1); - QCOMPARE_EQ(o1.deadlineTimeout(), o2.deadlineTimeout()); - - QGrpcCallOptions o3 = o1; - QCOMPARE_EQ(o1.deadlineTimeout(), o3.deadlineTimeout()); - - QGrpcCallOptions o4(std::move(o1)); - QCOMPARE_EQ(o4.deadlineTimeout(), o2.deadlineTimeout()); - - o1 = std::move(o4); - QCOMPARE_EQ(o1.deadlineTimeout(), o2.deadlineTimeout()); -} - -void QGrpcCallOptionsTest::hasImplicitQVariant() const -{ - QGrpcCallOptions o1; - o1.setDeadlineTimeout(250ms); - o1.setMetadata({ - { "keyA", "valA" }, - { "keyB", "valB" }, - }); - - QVariant v = o1; - QCOMPARE_EQ(v.metaType(), QMetaType::fromType()); - const auto o2 = v.value(); - QCOMPARE_EQ(o1.metadata(), o2.metadata()); - QCOMPARE_EQ(o1.deadlineTimeout(), o2.deadlineTimeout()); -} - -void QGrpcCallOptionsTest::hasMemberSwap() const -{ - constexpr std::chrono::milliseconds Dur = 50ms; - - QGrpcCallOptions o1; - o1.setDeadlineTimeout(Dur); - QGrpcCallOptions o2; - - QCOMPARE_EQ(o1.deadlineTimeout(), Dur); - QVERIFY(!o2.deadlineTimeout()); - o2.swap(o1); - QCOMPARE_EQ(o2.deadlineTimeout(), Dur); - QVERIFY(!o1.deadlineTimeout()); - swap(o2, o1); - QCOMPARE_EQ(o1.deadlineTimeout(), Dur); - QVERIFY(!o2.deadlineTimeout()); -} - -void QGrpcCallOptionsTest::propertyMetadata() const -{ - QHash md = { - { "keyA", "valA" }, - { "keyB", "valB" }, - }; - - QGrpcCallOptions o1; - auto o1Detach = o1; - o1.setMetadata(md); - QCOMPARE_EQ(o1.metadata(), md); - QCOMPARE_NE(o1.metadata(), o1Detach.metadata()); - - QGrpcCallOptions o2; - auto o2Detach = o2; - o2.setMetadata(std::move(md)); - QCOMPARE_EQ(o2.metadata(), o1.metadata()); - QCOMPARE_NE(o2.metadata(), o2Detach.metadata()); - - QCOMPARE_EQ(std::move(o1).metadata(), o2.metadata()); -} - -void QGrpcCallOptionsTest::propertyDeadline() const -{ - constexpr std::chrono::milliseconds Dur = 50ms; - - QGrpcCallOptions o1; - auto o1Detach = o1; - o1.setDeadlineTimeout(Dur); - QCOMPARE_EQ(o1.deadlineTimeout(), Dur); - QCOMPARE_NE(o1.deadlineTimeout(), o1Detach.deadlineTimeout()); -} - -void QGrpcCallOptionsTest::streamsToDebug() const -{ - QGrpcCallOptions o; - QString storage; - QDebug dbg(&storage); - dbg.noquote().nospace(); - - dbg << o; - QVERIFY(!storage.isEmpty()); - - std::unique_ptr ustr(QTest::toString(o)); - QCOMPARE_EQ(storage, QString::fromUtf8(ustr.get())); -} - QTEST_MAIN(QGrpcCallOptionsTest) #include "tst_qgrpccalloptions.moc" diff --git a/tests/auto/grpc/qgrpcchanneloptions/CMakeLists.txt b/tests/auto/grpc/qgrpcchanneloptions/CMakeLists.txt index 643fb118..6ba70100 100644 --- a/tests/auto/grpc/qgrpcchanneloptions/CMakeLists.txt +++ b/tests/auto/grpc/qgrpcchanneloptions/CMakeLists.txt @@ -10,6 +10,8 @@ endif() qt_internal_add_test(tst_qgrpcchanneloptions SOURCES tst_qgrpcchanneloptions.cpp + INCLUDE_DIRECTORIES + "${CMAKE_CURRENT_LIST_DIR}/../shared" LIBRARIES Qt::Core Qt::Test diff --git a/tests/auto/grpc/qgrpcchanneloptions/tst_qgrpcchanneloptions.cpp b/tests/auto/grpc/qgrpcchanneloptions/tst_qgrpcchanneloptions.cpp index df261c00..34865861 100644 --- a/tests/auto/grpc/qgrpcchanneloptions/tst_qgrpcchanneloptions.cpp +++ b/tests/auto/grpc/qgrpcchanneloptions/tst_qgrpcchanneloptions.cpp @@ -1,6 +1,8 @@ // Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +#include + #include #include @@ -8,116 +10,31 @@ #include -using namespace std::chrono_literals; - class QGrpcChannelOptionsTest : public QObject { Q_OBJECT private Q_SLOTS: - void hasSpecialMemberFunctions() const; - void hasImplicitQVariant() const; - void hasMemberSwap() const; - void propertyMetadata() const; - void propertyDeadline() const; + void hasSpecialMemberFunctions() const { common.hasSpecialMemberFunctions(); } + void hasImplicitQVariant() const { common.hasImplicitQVariant(); } + void hasMemberSwap() const { common.hasMemberSwap(); } +#if QT_DEPRECATED_SINCE(6, 13) + void deprecatedPropertyMetadata() const { common.deprecatedPropertyMetadata(); } + void propertyMetadataCompat() const { common.propertyMetadataCompat(); } +#endif + void propertyMetadata() const { common.propertyMetadata(); } + void propertyDeadline() const { common.propertyDeadline(); } + void streamsToDebug() const { common.streamsToDebug(); } + void propertySerializationFormat() const; #if QT_CONFIG(ssl) void propertySslConfiguration() const; #endif - void streamsToDebug() const; + +private: + GrpcCommonOptionsTest common; }; -void QGrpcChannelOptionsTest::hasSpecialMemberFunctions() const -{ - QGrpcChannelOptions o1; - QVERIFY(!o1.deadlineTimeout()); - QVERIFY(o1.metadata().empty()); - -#if QT_CONFIG(ssl) - QVERIFY(!o1.sslConfiguration()); -#endif - - o1.setDeadlineTimeout(100ms); - - QGrpcChannelOptions o2(o1); - QCOMPARE_EQ(o1.deadlineTimeout(), o2.deadlineTimeout()); - - QGrpcChannelOptions o3 = o1; - QCOMPARE_EQ(o1.deadlineTimeout(), o3.deadlineTimeout()); - - QGrpcChannelOptions o4(std::move(o1)); - QCOMPARE_EQ(o4.deadlineTimeout(), o2.deadlineTimeout()); - - o1 = std::move(o4); - QCOMPARE_EQ(o1.deadlineTimeout(), o2.deadlineTimeout()); -} - -void QGrpcChannelOptionsTest::hasImplicitQVariant() const -{ - QGrpcChannelOptions o1; - o1.setDeadlineTimeout(250ms); - o1.setMetadata({ - { "keyA", "valA" }, - { "keyB", "valB" }, - }); - - QVariant v = o1; - QCOMPARE_EQ(v.metaType(), QMetaType::fromType()); - const auto o2 = v.value(); - QCOMPARE_EQ(o1.metadata(), o2.metadata()); - QCOMPARE_EQ(o1.deadlineTimeout(), o2.deadlineTimeout()); -} - -void QGrpcChannelOptionsTest::hasMemberSwap() const -{ - constexpr std::chrono::milliseconds Dur = 50ms; - - QGrpcChannelOptions o1; - o1.setDeadlineTimeout(Dur); - QGrpcChannelOptions o2; - - QCOMPARE_EQ(o1.deadlineTimeout(), Dur); - QVERIFY(!o2.deadlineTimeout()); - o2.swap(o1); - QCOMPARE_EQ(o2.deadlineTimeout(), Dur); - swap(o2, o1); - QCOMPARE_EQ(o1.deadlineTimeout(), Dur); - QVERIFY(!o2.deadlineTimeout()); -} - -void QGrpcChannelOptionsTest::propertyMetadata() const -{ - QHash md = { - { "keyA", "valA" }, - { "keyB", "valB" }, - }; - - QGrpcChannelOptions o1; - auto o1Detach = o1; - o1.setMetadata(md); - QCOMPARE_EQ(o1.metadata(), md); - QCOMPARE_NE(o1.metadata(), o1Detach.metadata()); - - QGrpcChannelOptions o2; - auto o2Detach = o2; - o2.setMetadata(std::move(md)); - QCOMPARE_EQ(o2.metadata(), o1.metadata()); - QCOMPARE_NE(o1.metadata(), o2Detach.metadata()); - - QCOMPARE_EQ(std::move(o1).metadata(), o2.metadata()); -} - -void QGrpcChannelOptionsTest::propertyDeadline() const -{ - constexpr std::chrono::milliseconds Dur = 50ms; - - QGrpcChannelOptions o1; - auto o1Detach = o1; - o1.setDeadlineTimeout(Dur); - QCOMPARE_EQ(o1.deadlineTimeout(), Dur); - QCOMPARE_NE(o1.deadlineTimeout(), o1Detach.deadlineTimeout()); -} - void QGrpcChannelOptionsTest::propertySerializationFormat() const { QGrpcSerializationFormat fmt(QtGrpc::SerializationFormat::Json); @@ -147,20 +64,6 @@ void QGrpcChannelOptionsTest::propertySslConfiguration() const } #endif -void QGrpcChannelOptionsTest::streamsToDebug() const -{ - QGrpcChannelOptions o; - QString storage; - QDebug dbg(&storage); - dbg.nospace().noquote(); - - dbg << o; - QVERIFY(!storage.isEmpty()); - - std::unique_ptr ustr(QTest::toString(o)); - QCOMPARE_EQ(storage, QString::fromUtf8(ustr.get())); -} - QTEST_MAIN(QGrpcChannelOptionsTest) #include "tst_qgrpcchanneloptions.moc" diff --git a/tests/auto/grpc/shared/grpccommonoptions.h b/tests/auto/grpc/shared/grpccommonoptions.h new file mode 100644 index 00000000..689db084 --- /dev/null +++ b/tests/auto/grpc/shared/grpccommonoptions.h @@ -0,0 +1,237 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include + +#include + +#include +#include +#include + +using namespace std::chrono_literals; + +template +class GrpcCommonOptionsTest +{ +public: + void hasSpecialMemberFunctions() const + { + T o1; + QVERIFY(!o1.deadlineTimeout()); + QVERIFY(o1.metadata(QtGrpc::MultiValue).empty()); + + o1.setDeadlineTimeout(100ms); + + T o2(o1); + QCOMPARE_EQ(o1.deadlineTimeout(), o2.deadlineTimeout()); + + T o3 = o1; + QCOMPARE_EQ(o1.deadlineTimeout(), o3.deadlineTimeout()); + + T o4(std::move(o1)); + QCOMPARE_EQ(o4.deadlineTimeout(), o2.deadlineTimeout()); + + o1 = std::move(o4); + QCOMPARE_EQ(o1.deadlineTimeout(), o2.deadlineTimeout()); + } + void hasImplicitQVariant() const + { + T o1; + o1.setDeadlineTimeout(250ms); + o1.setMetadata({ + { "keyA", "valA" }, + { "keyB", "valB" }, + }); + + QVariant v = o1; + QCOMPARE_EQ(v.metaType(), QMetaType::fromType()); + const auto o2 = v.value(); + QCOMPARE_EQ(o1.metadata(QtGrpc::MultiValue), o2.metadata(QtGrpc::MultiValue)); + QCOMPARE_EQ(o1.deadlineTimeout(), o2.deadlineTimeout()); + } + void hasMemberSwap() const + { + constexpr std::chrono::milliseconds Dur = 50ms; + + T o1; + o1.setDeadlineTimeout(Dur); + T o2; + + QCOMPARE_EQ(o1.deadlineTimeout(), Dur); + QVERIFY(!o2.deadlineTimeout()); + o2.swap(o1); + QCOMPARE_EQ(o2.deadlineTimeout(), Dur); + QVERIFY(!o1.deadlineTimeout()); + swap(o2, o1); + QCOMPARE_EQ(o1.deadlineTimeout(), Dur); + QVERIFY(!o2.deadlineTimeout()); + } + +#if QT_DEPRECATED_SINCE(6, 13) +QT_WARNING_PUSH +QT_WARNING_DISABLE_DEPRECATED + + void deprecatedPropertyMetadata() const + { + QHash data = { + { "keyA", "valA" }, + { "keyB", "valB" }, + }; + + // cref setter + T o1; + auto o1Detach = o1; + o1.setMetadata(data); + QCOMPARE_EQ(o1.metadata(), data); + QCOMPARE_NE(o1.metadata(), o1Detach.metadata()); + + // rvalue setter + T o2; + auto o2Detach = o2; + auto dataMoved = data; + o2.setMetadata(std::move(dataMoved)); + QCOMPARE_EQ(o2.metadata(), data); + QCOMPARE_NE(o2.metadata(), o2Detach.metadata()); + + // rvalue-this getter + T o3; + o3.setMetadata(data); + auto movedMd = std::move(o3).metadata(); + QCOMPARE_EQ(movedMd, data); + } + void propertyMetadataCompat() const + { + auto toMulti = [](const QHash &m) { + return QMultiHash(m); + }; + QMultiHash multiMd = { + { "keyA", "valA1" }, + { "keyA", "valA2" }, + { "keyB", "valB1" }, + { "keyB", "valB2" }, + { "keyC", "valC" }, + }; + QHash md = { + { "keyA", "valA2" }, + { "keyB", "valB2" }, + { "keyC", "valC" }, + }; + + T o1; + o1.setMetadata(md); + + const auto &mdRef = o1.metadata(); + const auto &multiMdRef = o1.metadata(QtGrpc::MultiValue); + + QCOMPARE_EQ(mdRef, md); + QCOMPARE_EQ(multiMdRef, toMulti(mdRef)); + QCOMPARE_NE(typeid(mdRef), typeid(multiMdRef)); + + // Check that the handed out reference gets updates for QMultiHash setter + o1.setMetadata(multiMd); + QCOMPARE_EQ(multiMdRef, multiMd); + QCOMPARE_EQ(mdRef, md); + multiMd.insert("keyD", "valD"); + o1.setMetadata(multiMd); + QCOMPARE_EQ(multiMdRef, multiMd); + QCOMPARE_NE(mdRef, md); + md.insert("keyD", "valD"); + QCOMPARE_EQ(mdRef, md); + // Check that the handed out reference gets updates for QHash setter + o1.setMetadata(md); + QCOMPARE_EQ(multiMdRef, toMulti(md)); + QCOMPARE_EQ(mdRef, md); + + // Check shared state mutation due to lazy evaluation in metadata() + auto mdCopy = md; + T o2; + o2.setMetadata(mdCopy); + auto o2Detach = o2; + const auto &o2Md = o2.metadata(); + const auto &o2DetachMd = o2Detach.metadata(); + QCOMPARE_EQ(o2Md, mdCopy); + QCOMPARE_EQ(o2Md, o2DetachMd); + mdCopy.insert("keyX", "valX"); + o2.setMetadata(mdCopy); // trigger new merge + const auto &o2MdAfter = o2.metadata(); + const auto &o2DetachMdAfter = o2Detach.metadata(); + QCOMPARE_NE(o2MdAfter, o2DetachMdAfter); + + T o3A; + T o3B = o3A; + const auto &o3aMd = o3A.metadata(); + o3B.setMetadata(QHash{ + {"keyA","valA"}, {"keyB","valB"} + }); + // o3aMd is not affected by the update since o3B deprecatedQHashRef is used + QCOMPARE_NE(o3aMd, o3B.metadata()); + o3A.setMetadata(QHash{ + {"keyA","valA"}, {"keyB","valB"}, {"keyC","valC"}, + }); + // o3aMd is updated accoringly though + QCOMPARE_EQ(o3aMd, o3A.metadata()); + QCOMPARE_NE(o3B.metadata(), o3A.metadata()); + } + +QT_WARNING_POP +#endif + void propertyMetadata() const + { + std::initializer_list> list = { + { "keyA", "valA1" }, + { "keyA", "valA2" }, + { "keyB", "valB" }, + }; + QMultiHash data(list); + + // cref setter + T o1; + auto o1Detach = o1; + o1.setMetadata(data); + QCOMPARE_EQ(o1.metadata(QtGrpc::MultiValue), data); + QCOMPARE_NE(o1.metadata(QtGrpc::MultiValue), o1Detach.metadata(QtGrpc::MultiValue)); + + // rvalue setter + T o2; + auto o2Detach = o2; + auto dataMoved = data; + o2.setMetadata(std::move(dataMoved)); + QCOMPARE_EQ(o2.metadata(QtGrpc::MultiValue), data); + QCOMPARE_NE(o2.metadata(QtGrpc::MultiValue), o2Detach.metadata(QtGrpc::MultiValue)); + + // rvalue-this getter + T o3; + o3.setMetadata(data); + auto movedMd = std::move(o3).metadata(QtGrpc::MultiValue); + QCOMPARE_EQ(movedMd, data); + + // std::initializer_list setter + T o4; + o4.setMetadata(list); + QCOMPARE_EQ(o4.metadata(QtGrpc::MultiValue), data); + } + void propertyDeadline() const + { + constexpr std::chrono::milliseconds Dur = 50ms; + + T o1; + auto o1Detach = o1; + o1.setDeadlineTimeout(Dur); + QCOMPARE_EQ(o1.deadlineTimeout(), Dur); + QCOMPARE_NE(o1.deadlineTimeout(), o1Detach.deadlineTimeout()); + } + void streamsToDebug() const + { + T o; + QString storage; + QDebug dbg(&storage); + dbg.noquote().nospace(); + + dbg << o; + QVERIFY(!storage.isEmpty()); + + std::unique_ptr ustr(QTest::toString(o)); + QCOMPARE_EQ(storage, QString::fromUtf8(ustr.get())); + } +};