Add RdpSessionBackend test coverage (#1)

RdpSessionBackend can't reasonably get the same fixture-driven
state-machine tests SshSessionBackend got: it's driven by FreeRDP's own
event loop and a raw worker thread against a real freerdp_connect(), not
a QProcess we can point at a stand-in binary. What it does have is a
large amount of pure, regression-prone logic -- exactly the kind that
already caused a real historical bug here (the X11-keycode/PC-AT-scancode
mixup fixed in Milestone 7) -- so that's what gets covered instead.

Twelve functions promoted from free functions / private members to
public statics purely so tests can call them without a live connection:
security-mode/performance-profile normalization, the HiDPI scale-value
mapping, desktop-size clamping, both scancode-mapping functions, and the
five FreeRDP error-code interpretation functions. UINT32 is surfaced as
quint32 in the public signatures to keep FreeRDP/WinPR types out of the
header, matching how rdp_freerdp* is already only forward-declared there.

27 test cases, including a couple of direct regression guards: verifying
scancodeFromNativeScanCode() is a faithful passthrough to FreeRDP's X11
table (not a reimplementation), and that it does NOT reproduce the old
"X11 keycode treated as PC/AT scancode" bug for a documented example key.

This closes out #1's originally scoped work (CTest wiring, ProfileRepository,
SshSessionBackend, RdpSessionBackend coverage). Deeper state-machine
coverage for the two session backends remains future work if ever needed,
but isn't blocking here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 15:32:02 -06:00
co-authored by Claude Sonnet 5
parent 9842a44de0
commit 04ce6f7904
4 changed files with 383 additions and 61 deletions
+61 -59
View File
@@ -89,59 +89,13 @@ constexpr double kDefaultDpi = 96.0;
constexpr double kMillimetersPerInch = 25.4;
#ifdef ORBITHUB_HAS_FREERDP
QString normalizedRdpSecurityMode(const QString& value)
{
const QString mode = value.trimmed();
if (mode.compare(QStringLiteral("NLA"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("NLA");
}
if (mode.compare(QStringLiteral("TLS"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("TLS");
}
if (mode.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("RDP");
}
return QStringLiteral("Negotiate");
}
// MS-RDPEDISP restricts DesktopScaleFactor/DeviceScaleFactor to exactly
// these three values; FreeRDP's own reference client enforces the same
// set (client/common/cmdline.c, parse_scale_options). Anything else is
// silently ignored by the server, so map the real, continuous
// devicePixelRatio down to the nearest one.
UINT32 nearestFreeRdpScaleValue(qreal ratio)
{
if (ratio <= 1.2) {
return 100;
}
if (ratio <= 1.6) {
return 140;
}
return 180;
}
QString normalizedRdpPerformanceProfile(const QString& value)
{
const QString profile = value.trimmed();
if (profile.compare(QStringLiteral("Best Quality"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("Best Quality");
}
if (profile.compare(QStringLiteral("Best Performance"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("Best Performance");
}
if (profile.compare(QStringLiteral("Auto Detect"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("Auto Detect");
}
return QStringLiteral("Balanced");
}
bool applyRdpSecurityMode(rdpSettings* settings, const QString& mode)
{
if (settings == nullptr) {
return false;
}
const QString normalized = normalizedRdpSecurityMode(mode);
const QString normalized = RdpSessionBackend::normalizedRdpSecurityMode(mode);
BOOL rdp = FALSE;
BOOL tls = FALSE;
@@ -175,7 +129,7 @@ bool applyRdpPerformanceProfile(rdpSettings* settings, const QString& profile)
return false;
}
const QString normalized = normalizedRdpPerformanceProfile(profile);
const QString normalized = RdpSessionBackend::normalizedRdpPerformanceProfile(profile);
UINT32 connectionType = CONNECTION_TYPE_BROADBAND_HIGH;
BOOL networkAutoDetect = FALSE;
if (normalized == QStringLiteral("Best Quality")) {
@@ -875,8 +829,55 @@ BOOL orbitAuthenticateEx(freerdp* instance,
return TRUE;
}
}
UINT32 scancodeFromNativeScanCode(quint32 nativeScanCode)
QString RdpSessionBackend::normalizedRdpSecurityMode(const QString& value)
{
const QString mode = value.trimmed();
if (mode.compare(QStringLiteral("NLA"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("NLA");
}
if (mode.compare(QStringLiteral("TLS"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("TLS");
}
if (mode.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("RDP");
}
return QStringLiteral("Negotiate");
}
// MS-RDPEDISP restricts DesktopScaleFactor/DeviceScaleFactor to exactly
// these three values; FreeRDP's own reference client enforces the same
// set (client/common/cmdline.c, parse_scale_options). Anything else is
// silently ignored by the server, so map the real, continuous
// devicePixelRatio down to the nearest one.
quint32 RdpSessionBackend::nearestFreeRdpScaleValue(qreal ratio)
{
if (ratio <= 1.2) {
return 100;
}
if (ratio <= 1.6) {
return 140;
}
return 180;
}
QString RdpSessionBackend::normalizedRdpPerformanceProfile(const QString& value)
{
const QString profile = value.trimmed();
if (profile.compare(QStringLiteral("Best Quality"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("Best Quality");
}
if (profile.compare(QStringLiteral("Best Performance"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("Best Performance");
}
if (profile.compare(QStringLiteral("Auto Detect"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("Auto Detect");
}
return QStringLiteral("Balanced");
}
quint32 RdpSessionBackend::scancodeFromNativeScanCode(quint32 nativeScanCode)
{
if (nativeScanCode == 0) {
return RDP_SCANCODE_UNKNOWN;
@@ -935,10 +936,12 @@ UINT32 scancodeFromNativeScanCode(quint32 nativeScanCode)
#endif
}
UINT32 scancodeForQtKey(int key, Qt::KeyboardModifiers modifiers, quint32 nativeScanCode)
quint32 RdpSessionBackend::scancodeForQtKey(int key,
Qt::KeyboardModifiers modifiers,
quint32 nativeScanCode)
{
const bool keypad = modifiers.testFlag(Qt::KeypadModifier);
const UINT32 nativeScancode = scancodeFromNativeScanCode(nativeScanCode);
const quint32 nativeScancode = scancodeFromNativeScanCode(nativeScanCode);
switch (key) {
case Qt::Key_Escape:
@@ -1163,7 +1166,7 @@ UINT32 scancodeForQtKey(int key, Qt::KeyboardModifiers modifiers, quint32 native
}
}
QString mapRdpError(UINT32 code)
QString RdpSessionBackend::mapRdpError(quint32 code)
{
switch (code) {
case FREERDP_ERROR_CONNECT_LOGON_FAILURE:
@@ -1226,7 +1229,7 @@ QString mapRdpError(UINT32 code)
return QStringLiteral("RDP connection failed (0x%1).").arg(code, 8, 16, QChar('0'));
}
bool isExpectedDisconnectCode(UINT32 code)
bool RdpSessionBackend::isExpectedDisconnectCode(quint32 code)
{
switch (code) {
case FREERDP_ERROR_SUCCESS:
@@ -1256,7 +1259,7 @@ bool isExpectedDisconnectCode(UINT32 code)
}
}
bool isExpectedConnectAbortCode(UINT32 code)
bool RdpSessionBackend::isExpectedConnectAbortCode(quint32 code)
{
switch (code) {
case FREERDP_ERROR_SUCCESS:
@@ -1268,7 +1271,7 @@ bool isExpectedConnectAbortCode(UINT32 code)
}
}
QString disconnectMessageForCode(UINT32 code)
QString RdpSessionBackend::disconnectMessageForCode(quint32 code)
{
switch (code) {
case FREERDP_ERROR_IDLE_TIMEOUT:
@@ -1304,7 +1307,7 @@ QString disconnectMessageForCode(UINT32 code)
}
}
QString rdpErrorRaw(UINT32 code)
QString RdpSessionBackend::rdpErrorRaw(quint32 code)
{
const char* name = freerdp_get_last_error_name(code);
const QString text = (name != nullptr && name[0] != '\0') ? QString::fromUtf8(name)
@@ -1312,7 +1315,6 @@ QString rdpErrorRaw(UINT32 code)
return QStringLiteral("%1 (0x%2)").arg(text).arg(code, 8, 16, QChar('0'));
}
#endif
}
RdpSessionBackend::RdpSessionBackend(const Profile& profile, QObject* parent)
: SessionBackend(profile, parent),
@@ -2434,7 +2436,7 @@ void RdpSessionBackend::emitConnectionFailureAsync(const QString& displayMessage
Qt::QueuedConnection);
}
int RdpSessionBackend::sanitizeDesktopWidth(int width) const
int RdpSessionBackend::sanitizeDesktopWidth(int width)
{
if (width <= 0) {
return kDefaultDesktopWidth;
@@ -2442,7 +2444,7 @@ int RdpSessionBackend::sanitizeDesktopWidth(int width) const
return qBound(kMinDesktopWidth, width, kMaxDesktopWidth);
}
int RdpSessionBackend::sanitizeDesktopHeight(int height) const
int RdpSessionBackend::sanitizeDesktopHeight(int height)
{
if (height <= 0) {
return kDefaultDesktopHeight;
+18 -2
View File
@@ -19,6 +19,24 @@ public:
explicit RdpSessionBackend(const Profile& profile, QObject* parent = nullptr);
~RdpSessionBackend() override;
// Pure, state-free helpers exposed as public statics purely so tests
// can exercise them without a live FreeRDP connection. UINT32 values
// are surfaced as quint32 here to keep FreeRDP/WinPR types out of this
// header (uint32_t is what UINT32 always is on every platform this
// project targets).
static QString normalizedRdpSecurityMode(const QString& value);
static QString normalizedRdpPerformanceProfile(const QString& value);
static quint32 nearestFreeRdpScaleValue(qreal ratio);
static quint32 scancodeFromNativeScanCode(quint32 nativeScanCode);
static quint32 scancodeForQtKey(int key, Qt::KeyboardModifiers modifiers, quint32 nativeScanCode);
static QString mapRdpError(quint32 code);
static bool isExpectedDisconnectCode(quint32 code);
static bool isExpectedConnectAbortCode(quint32 code);
static QString disconnectMessageForCode(quint32 code);
static QString rdpErrorRaw(quint32 code);
static int sanitizeDesktopWidth(int width);
static int sanitizeDesktopHeight(int height);
public slots:
void connectSession(const SessionConnectOptions& options) override;
void disconnectSession() override;
@@ -124,8 +142,6 @@ public:
private:
void emitStateAsync(SessionState state, const QString& message);
void emitConnectionFailureAsync(const QString& displayMessage, const QString& rawMessage);
int sanitizeDesktopWidth(int width) const;
int sanitizeDesktopHeight(int height) const;
};
#endif