Long Live mockserver!

Our current test strategy relies on having a dedicated server running.
While this tests the most 'accurate' flow of the system, it is really
hard to verify the server behavior due to missing flexibility.

Introduce the mockserver to solve this problem. It wraps the low-level
async gRPC server API and allows for non-blocking processing of
operations. This allows it to run nicely together with the Qt Even Loop,
which is blocking.

Provide the server-handling code by using CallbackTags with Lambdas, or
by subclassing the AbstractRpcTag interface.

Change-Id: I086cbd1d6d8542c717ae036bdf54ba96c55afb58
Fixes: QTBUG-139285
Reviewed-by: Alexey Edelev <alexey.edelev@qt.io>
This commit is contained in:
Dennis Oberst 2025-08-15 13:47:29 +02:00
parent 60446214bb
commit bc052fbd51
11 changed files with 954 additions and 0 deletions

View File

@ -10,3 +10,6 @@ add_subdirectory(bidistream)
if(QT_FEATURE_ssl AND NOT WIN32) if(QT_FEATURE_ssl AND NOT WIN32)
add_subdirectory(ssl) add_subdirectory(ssl)
endif() endif()
if(TARGET grpc_mock_server)
add_subdirectory(end2end)
endif()

View File

@ -0,0 +1,68 @@
# Copyright (C) 2025 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)
cmake_minimum_required(VERSION 3.16)
project(tst_grpc_client_end2end LANGUAGES CXX)
find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST)
include("../shared/mockserver/CMakeLists.txt")
endif()
if(NOT TARGET grpc_mock_server)
message(WARNING "Dependencies of 'tst_grpc_client_end2end' not found. Skipping.")
return()
endif()
qt_internal_add_test(tst_grpc_client_end2end
SOURCES
tst_grpc_client_end2end.cpp
LIBRARIES
Qt::Test
Qt::Core
Qt::Grpc
grpc_mock_server
)
set(proto_files
"${CMAKE_CURRENT_LIST_DIR}/eventhub.proto"
"${CMAKE_CURRENT_LIST_DIR}/event.proto"
)
set(proto_import "${CMAKE_CURRENT_LIST_DIR}")
set(proto_out_include "${CMAKE_CURRENT_BINARY_DIR}/include")
set(proto_out_server "${proto_out_include}/proto/server")
set(proto_out_client "${proto_out_include}/proto/client")
qt_add_protobuf(tst_grpc_client_end2end
PROTO_FILES
${proto_files}
OUTPUT_DIRECTORY
${proto_out_client}
EXTRA_NAMESPACE "qt"
)
qt_add_grpc(tst_grpc_client_end2end CLIENT
PROTO_FILES
${proto_files}
OUTPUT_DIRECTORY
${proto_out_client}
EXTRA_NAMESPACE "qt"
)
protobuf_generate(
PROTOS ${proto_files}
TARGET tst_grpc_client_end2end
IMPORT_DIRS ${proto_import}
PROTOC_OUT_DIR ${proto_out_server}
)
protobuf_generate(
PROTOS ${proto_files}
TARGET tst_grpc_client_end2end
LANGUAGE grpc
GENERATE_EXTENSIONS .grpc.pb.h .grpc.pb.cc
PLUGIN "protoc-gen-grpc=\$<TARGET_FILE:gRPC::grpc_cpp_plugin>"
IMPORT_DIRS ${proto_import}
PROTOC_OUT_DIR "${proto_out_server}"
)
target_include_directories(tst_grpc_client_end2end PRIVATE
"${proto_out_include}"
)

View File

@ -0,0 +1,15 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
syntax = "proto3";
message Event {
enum Type {
UNKNOWN = 0;
SERVER = 1;
CLIENT = 2;
}
Type type = 1;
string name = 2;
uint64 number = 3;
}

View File

@ -0,0 +1,15 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
syntax = "proto3";
import "event.proto";
message None {}
service EventHub {
rpc Push(Event) returns (None) {}
rpc Subscribe(None) returns (stream Event) {}
rpc Notify(stream Event) returns (None) {}
rpc Exchange(stream Event) returns (stream Event) {}
}

View File

@ -0,0 +1,440 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <mockserver.h>
#include <proto/server/event.pb.h>
#include <proto/server/eventhub.grpc.pb.h>
#include <proto/client/event.qpb.h>
#include <proto/client/eventhub_client.grpc.qpb.h>
#include <QtGrpc/qgrpccalloptions.h>
#include <QtGrpc/qgrpcchanneloptions.h>
#include <QtGrpc/qgrpchttp2channel.h>
#include <QtTest/qsignalspy.h>
#include <QtTest/qtest.h>
#include <QtCore/qbytearray.h>
#include <QtCore/qhash.h>
#include <QtCore/qtimer.h>
#include <atomic>
#include <string>
#include <vector>
// Re-define so that we can use QTest Macros inside non void functions too.
#undef QTEST_FAIL_ACTION
#define QTEST_FAIL_ACTION \
do { \
std::cerr << "Test failed!" << std::endl; \
std::abort(); \
} while (0)
using namespace Qt::Literals::StringLiterals;
using MultiHash = QMultiHash<QByteArray, QByteArray>;
class QtGrpcClientEnd2EndTest : public QObject
{
Q_OBJECT
public:
static std::string serverHttpAddress() { return "localhost:50051"; }
static std::string serverHttpsAddress() { return "localhost:50052"; }
static std::string serverUnixAddress() { return "unix:///tmp/qtgrpc_test_end2end.sock"; }
static std::string serverUnixAbstractAddress() { return "unix-abstract:qtgrpc_test_end2end"; }
static std::vector<ListeningPort> serverListeningPorts()
{
return {
{ serverHttpAddress(), grpc::InsecureServerCredentials() },
#if QT_CONFIG(ssl)
{ serverHttpsAddress(), serverSslCredentials() },
#endif
#ifdef Q_OS_UNIX
{ serverUnixAddress(), grpc::InsecureServerCredentials() },
#endif
#ifdef Q_OS_LINUX
{ serverUnixAbstractAddress(), grpc::InsecureServerCredentials() },
#endif
};
}
private Q_SLOTS:
void initTestCase_data() const;
void initTestCase();
void cleanupTestCase();
void init();
void cleanup();
// Testcases:
void clientMetadataReceived_data() const;
void clientMetadataReceived();
void serverMetadataReceived_data() const;
void serverMetadataReceived();
void bidiStreamsInOrder();
private:
static std::shared_ptr<grpc::ServerCredentials> serverSslCredentials()
{
grpc::SslServerCredentialsOptions opts(GRPC_SSL_DONT_REQUEST_CLIENT_CERTIFICATE);
opts.pem_key_cert_pairs.push_back({ SslKey, SslCert });
return grpc::SslServerCredentials(opts);
}
private:
std::unique_ptr<MockServer> m_server;
std::unique_ptr<EventHub::AsyncService> m_service;
std::unique_ptr<qt::EventHub::Client> m_client;
};
void QtGrpcClientEnd2EndTest::initTestCase_data() const
{
QTest::addColumn<std::shared_ptr<QGrpcHttp2Channel>>("channel");
QUrl httpAddress("http://"_ba + QByteArrayView(serverHttpAddress()));
QTest::newRow("http") << std::make_shared<QGrpcHttp2Channel>(httpAddress);
#if QT_CONFIG(ssl)
QSslConfiguration tlsConfig;
tlsConfig.setProtocol(QSsl::TlsV1_2);
tlsConfig.setCaCertificates({ QSslCertificate{ QByteArray(SslCert) } });
tlsConfig.setAllowedNextProtocols({ "h2"_ba });
QGrpcChannelOptions chOpts;
chOpts.setSslConfiguration(tlsConfig);
QUrl httpsAddress("https://"_ba + QByteArrayView(serverHttpsAddress()));
QTest::newRow("https") << std::make_shared<QGrpcHttp2Channel>(httpsAddress, chOpts);
#endif
#ifdef Q_OS_UNIX
QUrl unixAddress(serverUnixAddress().data());
QTest::newRow("unix") << std::make_shared<QGrpcHttp2Channel>(unixAddress);
#endif
#ifdef Q_OS_LINUX
QUrl unixAbstractAddress(serverUnixAbstractAddress().data());
QTest::newRow("unix-abstract") << std::make_shared<QGrpcHttp2Channel>(unixAbstractAddress);
#endif
}
void QtGrpcClientEnd2EndTest::initTestCase()
{
QTest::failOnWarning();
m_service = std::make_unique<EventHub::AsyncService>();
m_server = std::make_unique<MockServer>();
QVERIFY(m_server->start(serverListeningPorts(), { m_service.get() }));
}
void QtGrpcClientEnd2EndTest::cleanupTestCase()
{
m_client.reset();
QVERIFY(m_server->stop());
m_service.reset();
}
void QtGrpcClientEnd2EndTest::init()
{
QVERIFY(m_service && m_server);
QFETCH_GLOBAL(std::shared_ptr<QGrpcHttp2Channel>, channel);
m_client = std::make_unique<qt::EventHub::Client>();
QVERIFY(m_client->attachChannel(channel));
}
void QtGrpcClientEnd2EndTest::cleanup()
{
m_client.reset();
QVERIFY(m_server->stopAsyncProcessing());
}
void QtGrpcClientEnd2EndTest::clientMetadataReceived_data() const
{
QTest::addColumn<MultiHash>("callMetadata");
QTest::addColumn<MultiHash>("channelMetadata");
MultiHash callMd{
{ "client-call-single", "call-value-1" },
{ "client-call-multi", "call-a" },
{ "client-call-multi", "call-b" }
};
MultiHash channelMd{
{ "client-channel-single", "channel-value-1" },
{ "client-channel-multi", "channel-a" },
{ "client-channel-multi", "channel-b" }
};
QTest::addRow("call") << callMd << MultiHash{};
QTest::addRow("channel") << MultiHash{} << channelMd;
QTest::addRow("call+channel") << callMd << channelMd;
}
void QtGrpcClientEnd2EndTest::clientMetadataReceived()
{
QFETCH(const MultiHash, callMetadata);
QFETCH(const MultiHash, channelMetadata);
// Setup Server-side handling
struct ServerData
{
grpc::ServerAsyncResponseWriter<None> op{ &ctx };
grpc::ServerContext ctx;
Event request;
None response;
};
auto *data = new ServerData;
CallbackTag *callHandler = new CallbackTag([&](bool ok) {
QVERIFY(ok);
const std::multimap<grpc::string_ref, grpc::string_ref>
&receivedMd = data->ctx.client_metadata();
auto mergedMd = channelMetadata;
mergedMd.unite(callMetadata);
for (auto it = mergedMd.cbegin(); it != mergedMd.cend(); ++it) {
// Check that each key-value pair sent by the client exists on the server
auto serverRange = receivedMd.equal_range(it.key().toStdString());
auto clientRange = mergedMd.equal_range(it.key());
QCOMPARE_EQ(std::distance(serverRange.first, serverRange.second),
std::distance(clientRange.first, clientRange.second));
while (clientRange.first != clientRange.second) {
// Look for the exact entry in the server range. The order may
// be changed but it must be present.
const auto it = std::find_if(serverRange.first, serverRange.second, [&](auto it) {
return it.first == clientRange.first.key().toStdString()
&& it.second == clientRange.first.value().toStdString();
});
QVERIFY(it != serverRange.second);
std::advance(clientRange.first, 1);
}
}
data->op.Finish(data->response, grpc::Status::OK, new DeleteTag<ServerData>(data));
return CallbackTag::Delete;
});
m_service->RequestPush(&data->ctx, &data->request, &data->op, m_server->cq(), m_server->cq(),
callHandler);
// Setup Client-side call
m_client->channel()->setChannelOptions(QGrpcChannelOptions().setMetadata(channelMetadata));
auto call = m_client->Push(qt::Event{}, QGrpcCallOptions().setMetadata(callMetadata));
QVERIFY(call);
connect(call.get(), &QGrpcOperation::finished, this, [&](const QGrpcStatus &status) {
QVERIFY(status.isOk());
auto response = call->read<qt::None>();
QVERIFY(response.has_value());
});
QVERIFY(m_server->startAsyncProcessing());
QSignalSpy finishedSpy(call.get(), &QGrpcOperation::finished);
QVERIFY(finishedSpy.isValid());
QVERIFY(finishedSpy.wait());
}
void QtGrpcClientEnd2EndTest::serverMetadataReceived_data() const
{
QTest::addColumn<bool>("filterServerMetadata");
QTest::addColumn<MultiHash>("expectedInitialMd");
QTest::addColumn<MultiHash>("expectedTrailingMd");
MultiHash initialMd{
{ "initial-1", "ivalue-1" },
{ "initial-2", "ivalue-2" }
};
MultiHash trailingMd{
{ "trailing-1", "tvalue-1" },
{ "trailing-multi", "tvalue-x" },
{ "trailing-multi", "tvalue-y" }
};
QTest::addRow("filter(true)") << true << initialMd << trailingMd;
QTest::addRow("filter(false)") << false << initialMd << trailingMd;
}
void QtGrpcClientEnd2EndTest::serverMetadataReceived()
{
using MultiHash = QMultiHash<QByteArray, QByteArray>;
QFETCH(const bool, filterServerMetadata);
QFETCH(const MultiHash, expectedInitialMd);
QFETCH(const MultiHash, expectedTrailingMd);
// Setup Server-side handling
struct ServerData
{
grpc::ServerAsyncResponseWriter<None> op{ &ctx };
grpc::ServerContext ctx;
Event request;
None response;
};
auto *data = new ServerData;
for (auto it = expectedInitialMd.cbegin(); it != expectedInitialMd.cend(); ++it)
data->ctx.AddInitialMetadata(it.key().toStdString(), it.value().toStdString());
for (auto it = expectedTrailingMd.cbegin(); it != expectedTrailingMd.cend(); ++it)
data->ctx.AddTrailingMetadata(it.key().toStdString(), it.value().toStdString());
CallbackTag *callHandler = new CallbackTag([&](bool ok) {
QVERIFY(ok);
data->op.Finish(data->response, grpc::Status::OK, new DeleteTag<ServerData>(data));
return CallbackTag::Delete;
});
m_service->RequestPush(&data->ctx, &data->request, &data->op, m_server->cq(), m_server->cq(),
callHandler);
// Setup Client-side call
auto chOpts = QGrpcChannelOptions().setFilterServerMetadata(filterServerMetadata);
m_client->channel()->setChannelOptions(chOpts);
auto call = m_client->Push(qt::Event{});
QVERIFY(call);
connect(call.get(), &QGrpcOperation::finished, this, [&](const QGrpcStatus &status) {
QVERIFY(status.isOk());
auto response = call->read<qt::None>();
QVERIFY(response.has_value());
const auto &initialMd = call->serverInitialMetadata();
const auto &trailingMd = call->serverTrailingMetadata();
if (filterServerMetadata) {
QCOMPARE(initialMd, expectedInitialMd);
QCOMPARE(trailingMd, expectedTrailingMd);
} else {
QCOMPARE_GE(initialMd.size(), expectedInitialMd.size());
QCOMPARE_GE(trailingMd.size(), expectedTrailingMd.size());
for (auto it = expectedInitialMd.cbegin(); it != expectedInitialMd.cend(); ++it)
QVERIFY(initialMd.contains(it.key(), it.value()));
for (auto it = expectedTrailingMd.cbegin(); it != expectedTrailingMd.cend(); ++it)
QVERIFY(trailingMd.contains(it.key(), it.value()));
}
});
QVERIFY(m_server->startAsyncProcessing());
QSignalSpy finishedSpy(call.get(), &QGrpcOperation::finished);
QVERIFY(finishedSpy.isValid());
QVERIFY(finishedSpy.wait());
}
void QtGrpcClientEnd2EndTest::bidiStreamsInOrder()
{
constexpr auto SleepTime = std::chrono::milliseconds(5);
// Setup Server-side handling
struct ServerData
{
grpc::ServerAsyncReaderWriter<Event, Event> op{ &ctx };
grpc::ServerContext ctx;
Event request;
Event response;
unsigned long count = 0;
std::atomic<bool> readerDone = false;
std::atomic<bool> writerDone = false;
void updateResponse()
{
response.set_type(Event::SERVER);
response.set_number(response.number() + 1);
response.set_name("server-" + std::to_string(response.number()));
}
};
auto *data = new ServerData;
CallbackTag *reader = new CallbackTag([&, current = 1u](bool ok) mutable {
if (!ok) {
data->readerDone = true;
if (data->writerDone)
data->op.Finish(grpc::Status::OK, new DeleteTag<ServerData>(data));
return CallbackTag::Delete;
}
QCOMPARE_EQ(data->request.type(), Event::CLIENT);
QCOMPARE_EQ(data->request.number(), current);
std::string name = "client-" + std::to_string(current);
QCOMPARE_EQ(data->request.name(), name);
++current;
data->op.Read(&data->request, reader);
return CallbackTag::Proceed;
});
CallbackTag *writer = new CallbackTag([&](bool ok) {
QVERIFY(ok);
if (data->response.number() >= data->count) {
data->writerDone = true;
if (data->readerDone)
data->op.Finish(grpc::Status::OK, new DeleteTag<ServerData>(data));
return CallbackTag::Delete;
}
std::this_thread::sleep_for(SleepTime);
data->updateResponse();
data->op.Write(data->response, writer);
return CallbackTag::Proceed;
});
CallbackTag *callHandler = new CallbackTag([&](bool ok) {
QVERIFY(ok);
const auto &md = data->ctx.client_metadata();
const auto countIt = md.find("call-count");
QVERIFY(countIt != md.cend());
data->count = std::stoul(std::string(countIt->second.data(), countIt->second.length()));
QCOMPARE_GT(data->count, 0);
data->op.Read(&data->request, reader);
data->updateResponse();
data->op.Write(data->response, writer);
return CallbackTag::Delete;
});
m_service->RequestExchange(&data->ctx, &data->op, m_server->cq(), m_server->cq(), callHandler);
// Client bidi stream
uint callCount = 25;
qt::Event request;
auto updateRequest = [&] {
request.setType(qt::Event::Type::CLIENT);
request.setNumber(request.number() + 1);
request.setName("client-"_L1 + QString::number(request.number()));
};
updateRequest();
auto copts = QGrpcCallOptions().addMetadata("call-count", QByteArray::number(callCount));
auto stream = m_client->Exchange(request, copts);
QVERIFY(stream);
connect(stream.get(), &QGrpcOperation::finished, this,
[](const QGrpcStatus &status) { QVERIFY(status.isOk()); });
connect(stream.get(), &QGrpcBidiStream::messageReceived, this, [&, current = 1u]() mutable {
const auto response = stream->read<qt::Event>();
QVERIFY(response.has_value());
QCOMPARE_EQ(response->type(), qt::Event::Type::SERVER);
QCOMPARE_EQ(response->number(), current);
QString name = "server-"_L1 + QString::number(current);
QCOMPARE_EQ(response->name(), name);
++current;
});
QTimer delayedWriter;
connect(&delayedWriter, &QTimer::timeout, this, [&, current = 1u]() mutable {
if (current >= callCount) {
stream->writesDone();
delayedWriter.stop();
}
updateRequest();
stream->writeMessage(request);
++current;
});
delayedWriter.start(SleepTime);
QVERIFY(m_server->startAsyncProcessing());
QSignalSpy finishedSpy(stream.get(), &QGrpcOperation::finished);
QVERIFY(finishedSpy.isValid());
QVERIFY(finishedSpy.wait());
}
QTEST_MAIN(QtGrpcClientEnd2EndTest)
#include "tst_grpc_client_end2end.moc"

View File

@ -11,3 +11,4 @@ else()
endif() endif()
add_subdirectory(client_service) add_subdirectory(client_service)
add_subdirectory(client_test_common) add_subdirectory(client_test_common)
add_subdirectory(mockserver)

View File

@ -0,0 +1,34 @@
# Copyright (C) 2025 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE)
find_package(Protobuf)
find_package(gRPC)
if(CMAKE_CROSSCOMPILING)
find_program(grpc_cpp_plugin grpc_cpp_plugin NO_CACHE)
elseif(TARGET gRPC::grpc_cpp_plugin)
set(grpc_cpp_plugin $<TARGET_FILE:gRPC::grpc_cpp_plugin>)
else()
set(grpc_cpp_plugin "")
endif()
if(NOT grpc_cpp_plugin OR NOT TARGET WrapProtoc::WrapProtoc
OR NOT TARGET gRPC::grpc++)
message(WARNING "Dependencies of 'grpc_mock_server' not found. Skipping.")
return()
endif()
add_library(grpc_mock_server STATIC mockserver.h mockserver.cpp)
target_include_directories(grpc_mock_server
PUBLIC
"${CMAKE_CURRENT_LIST_DIR}"
)
target_link_libraries(grpc_mock_server
PUBLIC
protobuf::libprotobuf
gRPC::grpc++
)

View File

@ -0,0 +1,98 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#pragma once
#include <string>
// Valid for the next 100 years.
inline const static std::string SslCert = R"(
-----BEGIN CERTIFICATE-----
MIIFpDCCA4wCCQCbPKhPsY/FVjANBgkqhkiG9w0BAQsFADCBkzELMAkGA1UEBhMC
REUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVybGluMRwwGgYDVQQKDBNU
aGUgUXQgQ29tcGFueSBHbWJIMQwwCgYDVQQLDANSbkQxEjAQBgNVBAMMCWxvY2Fs
aG9zdDEiMCAGCSqGSIb3DQEJARYTZGVubmlzLm9iZXJzdEBxdC5pbzAeFw0yNTA4
MjExMzQ5MjFaFw0zNTA4MTkxMzQ5MjFaMIGTMQswCQYDVQQGEwJERTEPMA0GA1UE
CAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xHDAaBgNVBAoME1RoZSBRdCBDb21w
YW55IEdtYkgxDDAKBgNVBAsMA1JuRDESMBAGA1UEAwwJbG9jYWxob3N0MSIwIAYJ
KoZIhvcNAQkBFhNkZW5uaXMub2JlcnN0QHF0LmlvMIICIjANBgkqhkiG9w0BAQEF
AAOCAg8AMIICCgKCAgEA5kkaxZi/RtgEOWm7CmFvRlocBMtAPumZSvHCYxQXNvp7
biombepztFejuNzdO6Xaz1IbQhDmzRv9PAQFvOYGLWCfV8oMeZE+maQNIBVSxIaK
c7o2AZUA0z1AROLpfrn+RbhTfTrXiY5FVheMJZIZC5EvdLCBPmlSpE1ILk1otkcl
ID6EMsVAhTdFRP3CJZtAaGFivLsJmgkXHgmGT+pGY4eknIhuRMCWk+mnyuqbI5rf
H4eXTb2mtXTtiOy4CNd7N3d2m4su1OiYYl8yo8iZRS6XV3NCs73QaEoTbEoh0YR+
m88xxUsl42AWUnNn+A4V3p4SpHZUwDLC+MBVVuB/AlTrGVTMmztYuUhkZH9WTwn4
+cezAO925Wei0tkAGkqYvYLUFxEzgT8u+x0WpcbvQvvkf2/l4UCadw6k1UEWmyFT
+mHe1uYYucK+Qmj/lPkHgipyDDSzOKCU2r6GCxkl7ctCnUIUyYdiw0aMkJq2gOyM
TPXT9XXFumeK3jnv7/4Ly4UivhediwhJXRI2aMOctcK3zgQ0yKe4T8Sjao9un4Fb
euvtp9+CXxj2tEst1gQKn/kB0VHa4WqnC6TaZug7hTwwYvmwApvAnSEK5ach8oJR
hgfPctVHTdkpOkvrF8xxQW/3+ip3LawZm17egUXx+0JNNw7yA+FRcRFxLN858W0C
AwEAATANBgkqhkiG9w0BAQsFAAOCAgEAkSN4aIkGfVgi9XBZeja78lMnfyUD9lkO
NSZ2XS+dKUi2xOWGMzWnD6lqOLKxAUDeQVH+M/Y+8xwoVsiRfbtONRjHcUKa8YR/
Z0BzU4CLohFJGLwxSJtjoIiHVF9io9GAjsG7heawqhuAyUHnjAuqQbad3AIiTzIr
lheH2zQUVd+ld2ozLQinHvEGcTOl6U5TgNQVa75ojgdYBeRH6ewuKQt0nSCRXd+K
CtImwWPKjpbdcS5vom7U+1SGsz0/MvaLXq7DoMjaLXMdljrEEjTUjuhEJ3Ld7DBH
UeColH1wuZVyaHEJNRdKb/VjqC2SKdSHT+uvkuTlWq+lY3vyV/tAjUsRmhf539Xh
fpA1Ok9GSwtAD45tV/aHblxtogI0R4xKA+CCF/c5FYsl4cX9dSVq7Nyxfb0ZvP+S
8aKrqy64XnvEVotUQIP/qZspJgE9pbgnJ7cm6TL+D73OghV+6tvhgiYM4UA7oRJZ
pAqyCLhjTgv1KEGQQ5b7dRFvbt6IbmmdetOjOICVhko6Rz2/M3fIS7EvY12Vuhs0
IUjB00f4dn5CD+2zyv74BNIesyk2YEfaqck7CrAjbz69MNjoTvYdMRr4o0eN8yVD
MUnTeeMKniNQ2aTUMPpTnTIPwh4ih44U50HIZITG/6nvlHTTzRbSQa+Jw4OAW8vs
UCLjQ0fpqT0=
-----END CERTIFICATE-----
)";
inline const static std::string SslKey = R"(
-----BEGIN PRIVATE KEY-----
MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDmSRrFmL9G2AQ5
absKYW9GWhwEy0A+6ZlK8cJjFBc2+ntuKiZt6nO0V6O43N07pdrPUhtCEObNG/08
BAW85gYtYJ9Xygx5kT6ZpA0gFVLEhopzujYBlQDTPUBE4ul+uf5FuFN9OteJjkVW
F4wlkhkLkS90sIE+aVKkTUguTWi2RyUgPoQyxUCFN0VE/cIlm0BoYWK8uwmaCRce
CYZP6kZjh6SciG5EwJaT6afK6psjmt8fh5dNvaa1dO2I7LgI13s3d3abiy7U6Jhi
XzKjyJlFLpdXc0KzvdBoShNsSiHRhH6bzzHFSyXjYBZSc2f4DhXenhKkdlTAMsL4
wFVW4H8CVOsZVMybO1i5SGRkf1ZPCfj5x7MA73blZ6LS2QAaSpi9gtQXETOBPy77
HRalxu9C++R/b+XhQJp3DqTVQRabIVP6Yd7W5hi5wr5CaP+U+QeCKnIMNLM4oJTa
voYLGSXty0KdQhTJh2LDRoyQmraA7IxM9dP1dcW6Z4reOe/v/gvLhSK+F52LCEld
EjZow5y1wrfOBDTIp7hPxKNqj26fgVt66+2n34JfGPa0Sy3WBAqf+QHRUdrhaqcL
pNpm6DuFPDBi+bACm8CdIQrlpyHyglGGB89y1UdN2Sk6S+sXzHFBb/f6KnctrBmb
Xt6BRfH7Qk03DvID4VFxEXEs3znxbQIDAQABAoICAQDiCKl9gBNnbwqea/hKFR8K
t9G+pt21osZzOF9rrsGmli/nDvpPcwwE3Oz3u9pu/LmMO3RD4aEZfDqQ2QXkxwcT
LT7aBZk/DeCbH9o+Po/SFJj7RLBT1zRLI1jdBLjZSaiaOHXCeqoq+3l1KoHGMuPg
Za5l5AXIA8s5OB38TMDWAXkgcByEVPaii4CzWjxhe0S577ThuNiQ2BFXy1OJR1O1
x+M8PfG2DC8Amhy3YJXMWexd31IU3W0vuMiaWHe/PfpUlC7YN8JM+szv6a8j8fb3
X+bu0FSNZmeUpfjwlschBuLa/oFEvSFAUAU5AsvvP6wZqaB0yy0exfR4AxkAwDQI
ccFdJHGuoqmGYnLD+RmwfMJ2rbgt/zGbzvcxPtoFGBjmznjc7os/zRDNrytv2muu
vY23g2dSiZwC4aOk1ySflb9/+P7pIzZiDwppOKLkHEDa4Qlz5EyDMTI8DGYCjkfT
Un+3dJtgmHM5JpJ7tfvXJ0S6Q0Y6NRjpetzPJV2ylZsy86mBi6/N3baPc9DO1gqF
AjhKz0CLbaa2+VLs/dBhpRORtXAjv0PR7Z0xZsnv5TzF6/YK6U0pYIm2+RA/7g3Y
FQdfnehwixY9RQivE45NzijgItY2Nyb9IDHZ8J64z+ArkIlXMd4nIjnzNHQK5Rs0
JhJN5Q6F5bxzl8xpj6wpwQKCAQEA/dffOSyE1bxB8hqo627nhJRyRxdyHtkjKv9o
kqrvlxka8VsVSa3unlU9wIPx56KwrVs3MEYj66HyHimgt0nSg77gvqnAl832zDRZ
Bmw9qwMd2zzMRHXn2hpYfNJu0g7zemvp9ZxDrjR4KvEhML/IhY3TbOL49SQxvIZ9
/5I7FW5emvevPAzVWD9UrbrdevmELYjDUMNo3v3UaChpD0IL7db6lI5eI7TXmrdK
VyCLz8lFrhaH9SDskjBu5mLiB+Inwa4eAH73Sh2OwnG22JOfsPkPVIuqEc7xbdXM
IubgiiTGfNF6zS+yJuZAzNkzeX+zDCM4Rgvz9YObWle/czKEPQKCAQEA6D3+LcsQ
R1+AX/9wSnwBuznQVWGOLDW34y/32OMGtWafis7c16ys89skO2wFKlyHgk5Qs2vF
+NfiUtpA8JS4HUgYPySieSGOFmbr4Eryq5xwPaoQsFgoC2miUXwXHvICHl1J9aq+
1HUrOkCGzPGeKv02CSoMgeRevDVGJgbOj3SSa24PEmI/t2JLDOi0J8/QQc79C2du
JFb83ARrI1KzKTlMZg7LZ8s3YHOTRzvKHzE15lAyDEbzBy4mAvS/SuzqKxCgosz+
XOyUPIhZMX4wQak1vcYQ/TPOYVdMSpghYFt0Oa+1o2M7ktKaNzEaTBLsnU6m76iy
J7hzZYYrniyE8QKCAQBgYvji59GkqvBLcu4TP7BvekKOVgvCROcCq6rUjk2djS3a
2aASsxW5T1q8YB2Zu//kQ4+IKAn5riuWYm4hSsnBttf17dUwj0eYMIRhWPZtmihb
GBHkKPUZrwlMlEb6Qi0XniRfW6+jVU8P1zGoJhqJA9p1LRYlV1H/aP4s7iS7NYZ8
x3HllmXoNVv8/8ibqmUTOSwY3apTigR+bGHAJm8LJ6dMg2ahnkiD+fcjcDtGcgGg
YfPME21g8T0bBA8ZuTkpZOkFfTB/FwwfLzijsiJf+6JjkwjH7FFmSFlUI2C9c8te
l9hOUz8NYD7Yydyu1Ntyz2jNyDohTpDN1CXhIxxtAoIBAQClADZwjo84Rk/M5I0B
Pm10ebTclH0QR/IoBVKP02xWwVykoCgjS1ltv8pUNYDOAgN8vutzLiTvkWII/2uY
AfF1TF19ryeH7CEpJWJ8boNPDcxo40UMJPX+dcSPJBzBLav3qsv7MJ54D+7wahvb
y+ZWIKSdijRsrLXp/eei6L8lCOtmTEGFDCy8u6cautIUXv9VYhxCV+/W/b6VCsab
yAvmRAFVmTlGuwjTDAodWAtYcfwbb+q+8kGEXnM/MmgecYhFpICcagxmNOd0wmKs
WT0ryW6XL/uuCXqdigp4DPHZpeBDg+UG+l+/BsXuZIKMff2CHflY2IAK3G+QN+0R
agvhAoIBAQDaRdk4Rj8n+9v88gwoTX1inBFUSUTYv85YzoVlkto7D0LGHasACrxq
l3PNE4retkTLCLkY36ji7u2QOhQaOb1/awzOw5ZQd6az/nC3jBHfNZUUC5ZSzFJx
+xj5Vh17fegmXcmSx3zqOGfgs5ix8Fm4BcAWVycbQ5JtLBlmuvT8Nl+Xgs5Vmiid
H5/MPxQxZ5kp7rIQBkEIhl9fwpwfVbpfC6HClUsSvVIkrOCMdgBt06K7RaIsEkPy
Ce8q6TZu0/k9P2o1ubZIyUWIhqrLB2J8F4TNAACqi9beR9uMQfly2CXPzo1/yT51
lQREmzyAXi5cEA2KzhcGKK6cfV1ZgUA6
-----END PRIVATE KEY-----
)";

View File

@ -0,0 +1,133 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include "mockserver.h"
#include <grpcpp/alarm.h>
#include <grpcpp/server_builder.h>
MockServer::MockServer() = default;
MockServer::~MockServer()
{
stop();
};
bool MockServer::start(std::vector<ListeningPort> ports, std::vector<grpc::Service *> services)
{
if (!transitionState(State::Stopped, State::Starting))
return false;
grpc::ServerBuilder builder;
for (auto &p : ports)
builder.AddListeningPort(p.addressUri, p.creds, &p.selectedPort);
for (auto *s : services)
builder.RegisterService(s);
mCQ = builder.AddCompletionQueue();
mServer = builder.BuildAndStart();
if (!mServer || !mCQ) {
mState = State::Stopped;
return false;
}
mState = State::Started;
return true;
}
bool MockServer::stop()
{
State currentState = mState.load();
if (currentState == State::Processing)
stopAsyncProcessing();
if (currentState != State::Started && currentState != State::Processing)
return currentState == State::Stopped;
mState = State::ShuttingDown;
mServer->Shutdown(std::chrono::system_clock::now() + std::chrono::milliseconds(500));
mCQ->Shutdown();
void *drainTag;
bool drainOk;
while (mCQ->Next(&drainTag, &drainOk)) {
if (drainTag)
static_cast<AbstractTag*>(drainTag)->process(drainOk);
}
mServer.reset();
mCQ.reset();
mState = State::Stopped;
return true;
}
bool MockServer::processTag(int timeoutMs)
{
State currentState = mState.load();
if (currentState != State::Processing && currentState != State::Started) {
return false;
}
void *rawTag = nullptr;
bool ok = false;
const auto status = timeoutMs < 0
? mCQ->AsyncNext(&rawTag, &ok, gpr_inf_future(GPR_CLOCK_REALTIME))
: mCQ->AsyncNext(&rawTag, &ok,
std::chrono::system_clock::now() + std::chrono::milliseconds(timeoutMs));
if (rawTag && status == grpc::CompletionQueue::NextStatus::GOT_EVENT) {
static_cast<AbstractTag *>(rawTag)->process(ok);
return true;
}
return false;
}
bool MockServer::startAsyncProcessing(int timeoutMs)
{
if (!transitionState(State::Started, State::Processing))
return false;
mProcessingThread = std::thread([this, timeoutMs] {
while (mState.load(std::memory_order_acquire) == State::Processing) {
processTag(timeoutMs);
}
});
return true;
}
bool MockServer::stopAsyncProcessing()
{
if (!transitionState(State::Processing, State::Started))
return false;
if (mProcessingThread.joinable()) {
grpc::Alarm alarm;
// Trigger an event so that the processing loop detects the change.
alarm.Set(mCQ.get(), gpr_now(gpr_clock_type::GPR_CLOCK_REALTIME), new VoidTag());
mProcessingThread.join();
}
return true;
}
MockServer &MockServer::step(int timeoutMs)
{
mFutures.emplace_back(std::async(std::launch::async,
[this, timeoutMs] { return processTag(timeoutMs); }));
return *this;
}
bool MockServer::waitForAllSteps()
{
for (auto &f : mFutures) {
if (!f.get())
return false;
}
mFutures.clear();
return true;
}
bool MockServer::transitionState(State from, State to)
{
State expected = from;
return mState.compare_exchange_strong(expected, to);
}

View File

@ -0,0 +1,63 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#pragma once
#include "tags.h"
#include "certificates.h"
#include <grpcpp/grpcpp.h>
#include <grpcpp/security/server_credentials.h>
#include <memory>
#include <string>
#include <vector>
#include <thread>
#include <future>
struct ListeningPort
{
std::string addressUri;
std::shared_ptr<grpc::ServerCredentials> creds;
int selectedPort = -1;
};
class MockServer
{
public:
enum class State {
Stopped,
Starting,
Started,
Processing,
ShuttingDown,
};
MockServer();
~MockServer();
grpc::ServerCompletionQueue *cq() { return mCQ.get(); }
State state() const { return mState.load(std::memory_order_acquire); }
bool start(std::vector<ListeningPort> ports, std::vector<grpc::Service *> services);
bool stop();
bool processTag(int timeoutMs = -1);
bool startAsyncProcessing(int timeoutMs = -1);
bool stopAsyncProcessing();
void startRpcTag(AbstractRpcTag &tag) { tag.start(mCQ.get()); }
MockServer &step(int timeoutMs = -1);
bool waitForAllSteps();
private:
bool transitionState(State from, State to);
private:
std::unique_ptr<grpc::Server> mServer;
std::unique_ptr<grpc::ServerCompletionQueue> mCQ;
std::vector<std::future<bool>> mFutures;
std::thread mProcessingThread;
std::atomic<State> mState = State::Stopped;
};

View File

@ -0,0 +1,84 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#pragma once
#include <grpcpp/completion_queue.h>
#include <grpcpp/server_context.h>
#include <atomic>
#include <functional>
class AbstractTag
{
public:
explicit AbstractTag() = default;
virtual ~AbstractTag() = default;
AbstractTag(const AbstractTag &) = delete;
AbstractTag &operator=(const AbstractTag &) = delete;
AbstractTag(AbstractTag &&) = default;
AbstractTag &operator=(AbstractTag &&) = default;
virtual void process(bool ok) = 0;
};
class CallbackTag : public AbstractTag
{
public:
enum Operation { Proceed, Delete };
using Function = std::function<Operation(bool)>;
explicit CallbackTag(Function fn) : mFn(std::move(fn)) { }
void process(bool ok) override
{
if (mFn(ok) == Delete)
delete this;
}
private:
Function mFn;
};
class VoidTag final : public CallbackTag
{
public:
VoidTag() : CallbackTag([](bool) { return Delete; }) { }
};
template <typename Data>
class DeleteTag final : public CallbackTag
{
public:
explicit DeleteTag(Data *data) : CallbackTag([](bool) { return Delete; }), data(data)
{
assert(this->data);
}
~DeleteTag() { delete data; }
private:
Data *data;
};
class AbstractRpcTag : public AbstractTag
{
public:
AbstractRpcTag()
{
mContext.AsyncNotifyWhenDone(new CallbackTag([this](bool ok) {
if (ok && mContext.IsCancelled())
mIsCancelled = true;
return CallbackTag::Delete;
}));
}
virtual void start(grpc::ServerCompletionQueue *cq) = 0;
const grpc::ServerContext &context() const { return mContext; }
grpc::ServerContext &context() { return mContext; }
[[nodiscard]] bool isCancelled() const noexcept { return mIsCancelled.load(); }
private:
std::atomic<bool> mIsCancelled = false;
grpc::ServerContext mContext;
};