mudlet/test/functional_tests/TelnetSgrDefaultColorTest.cpp
Vadim Peretokin b741663a1c
infrastructure: drop comments that restate the code beside them (#9681)
#### Brief overview of PR changes/additions
- Removed 8 comments that only repeated the statement or assertion
message next to them
- Kept 1 of the 16 identical copies of the `lua_next()` key-copy note in
`TLuaInterpreterMedia.cpp`
- Comment-only: zero code lines changed

#### Motivation for adding to Mudlet
Reading a comment and then the code that says the same thing is wasted
effort; the rationale comments that document real gotchas are all
untouched.

#### Other info (issues closed, discussion etc)
Result of a pass over the last month of commits on `development`. The
vast majority of comments added there explain *why* rather than restate
*what*, so this is deliberately a small diff.

**Test case:** `git diff development...HEAD` shows only comment lines
removed; build and test suites are unaffected.


Assisted-by: Claude:claude-opus-5
Signed-off-by: Vadim Peretokin <vadim.peretokin@mudlet.org>
2026-08-11 08:06:16 +02:00

284 lines
11 KiB
C++

/***************************************************************************
* Copyright (C) 2026 by Mudlet Developers *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
/*
* Tests for SGR 39 (default foreground) / SGR 49 (default background)
* handling - https://github.com/Mudlet/Mudlet/issues/9466
*
* After "CSI 31 m" (red) followed by "CSI 39 m" (default foreground), a
* later "CSI 1 m" (bold) must not resurrect the previous color as its
* bright variant; bold default-color text renders with a bold font weight
* like other terminal emulators do.
*
* Uses loopbackTest() to inject data directly into the telnet processing
* pipeline. Mudlet is started once in initTestCase and reused across all
* tests.
*
* Run with: ctest -R TelnetSgrDefaultColorTest -V
*/
#include <QtTest/QtTest>
#include <chrono>
#include "MudletInstanceCoordinator.h"
#include "TMainConsole.h"
#include "TelnetServerStub.h"
#include "ctelnet.h"
#include "dlgConnectionProfiles.h"
#include "mudlet.h"
using namespace std::chrono_literals;
extern void qInitResources_mudlet();
extern void qInitResources_qm();
extern void qInitResources_additional_splash_screens();
extern void qInitResources_mudlet_fonts_common();
extern void qInitResources_mudlet_fonts_posix();
void initializeQRCResourcesForSgrDefaultColorTest();
class TelnetSgrDefaultColorTest : public QObject
{
Q_OBJECT
private:
TelnetServerStub* mpServer = nullptr;
Host* mpHost = nullptr;
const QString mHostname = qsl("SGR-Default-Color-Test-Host");
QString mPort; // assigned the stub's actual ephemeral port in initTestCase()
const QString mLocalhost = qsl("localhost");
// Injects raw telnet data into the processing pipeline via loopback and
// waits for the buffer to process it.
void injectData(const QByteArray& data)
{
QByteArray terminated = data + QByteArrayLiteral("\r\n");
mpHost->mTelnet.loopbackTest(terminated);
QTest::qWait(50ms);
}
// Returns the TChar of the first character of the first occurrence of
// segment in the buffer, or std::nullopt if not found.
std::optional<TChar> charOfSegment(const QString& segment)
{
TMainConsole* console = mpHost->mpConsole;
for (int line = 0; line <= console->buffer.getLastLineNumber(); ++line) {
const int col = console->buffer.line(line).indexOf(segment);
if (col >= 0) {
return console->buffer.buffer.at(line).at(col);
}
}
return std::nullopt;
}
private slots:
// Start mudlet and create a profile once for all tests.
void initTestCase()
{
initializeQRCResourcesForSgrDefaultColorTest();
mpServer = new TelnetServerStub(qApp);
mpServer->start(mLocalhost, 0); // ephemeral OS-assigned port avoids collisions across concurrent test runs
mPort = QString::number(mpServer->serverPort());
mudlet::start();
mudlet::self()->setupConfig();
mudlet::self()->takeOwnershipOfInstanceCoordinator(std::make_unique<MudletInstanceCoordinator>("MudletInstanceCoordinator"));
mudlet::self()->init();
mudlet::self()->setStorePasswordsSecurely(false);
const QString path = mudlet::getMudletPath(enums::profileHomePath, mHostname);
QDir(path).removeRecursively();
QTimer::singleShot(0ms, qApp, [this]() {
mudlet::self()->startAutoLogin({});
QTest::qWait(100ms);
QTest::mouseClick(mudlet::self()->mpConnectionDialog->new_profile_button, Qt::LeftButton);
QTest::qWait(100ms);
QTest::keyClicks(QApplication::focusWidget(), mHostname);
QTest::qWait(100ms);
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Tab);
QTest::qWait(100ms);
QTest::keyClicks(QApplication::focusWidget(), mLocalhost);
QTest::qWait(100ms);
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Tab);
QTest::qWait(100ms);
QTest::keyClicks(QApplication::focusWidget(), mPort);
QTest::qWait(100ms);
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Return);
});
QSignalSpy spy(mudlet::self(), &mudlet::signal_profileLoaded);
if (!spy.wait(1000)) {
QFAIL("Profile took too long to load.");
}
mpHost = mudlet::self()->getActiveHost();
if (!mpHost) {
QFAIL("No active host available for the test.");
}
QSignalSpy spy2(&(mpHost->mTelnet), &cTelnet::signal_connected);
if (!spy2.wait(500)) {
QFAIL("Could not connect with the host.");
}
}
// Clear the buffer and reset SGR state before each test for isolation.
void init()
{
QVERIFY(mpHost);
QVERIFY(mpHost->mpConsole);
mpHost->mpConsole->buffer.clear();
mpHost->mpConsole->buffer.resetCurrentTextFormat();
}
// The reported bug: red, then default foreground, then bold - the bold
// text must be the default color in a bold font, not bright red.
void boldAfterDefaultForegroundReset()
{
injectData(QByteArrayLiteral("\x1b[31mred \x1b[39mplain \x1b[1mbold"));
auto boldChar = charOfSegment(qsl("bold"));
QVERIFY(boldChar.has_value());
QVERIFY(boldChar->foreground() != mpHost->mLightRed);
QCOMPARE(boldChar->foreground(), mpHost->mFgColor);
QVERIFY(boldChar->isBold());
}
// The working comparison from the bug report: a full SGR 0 reset instead
// of SGR 39 behaves the same way.
void boldAfterFullReset()
{
injectData(QByteArrayLiteral("\x1b[31mred \x1b[0mplain \x1b[1mbold"));
auto boldChar = charOfSegment(qsl("bold"));
QVERIFY(boldChar.has_value());
QCOMPARE(boldChar->foreground(), mpHost->mFgColor);
QVERIFY(boldChar->isBold());
}
// SGR 39 must not stop a subsequent color change from working, including
// its brightening by a following bold.
void newColorAfterDefaultForegroundReset()
{
injectData(QByteArrayLiteral("\x1b[31ma \x1b[39mb \x1b[31magain \x1b[1mbright"));
auto againChar = charOfSegment(qsl("again"));
QVERIFY(againChar.has_value());
QCOMPARE(againChar->foreground(), mpHost->mRed);
auto brightChar = charOfSegment(qsl("bright"));
QVERIFY(brightChar.has_value());
QCOMPARE(brightChar->foreground(), mpHost->mLightRed);
}
void boldColorStillBrightens()
{
injectData(QByteArrayLiteral("\x1b[31;1mbright"));
auto brightChar = charOfSegment(qsl("bright"));
QVERIFY(brightChar.has_value());
QCOMPARE(brightChar->foreground(), mpHost->mLightRed);
}
// SGR 39 arriving while bold is already active: the text switches to the
// default color right away.
void defaultForegroundResetWhileBold()
{
injectData(QByteArrayLiteral("\x1b[1m\x1b[31mbright \x1b[39mafter"));
auto brightChar = charOfSegment(qsl("bright"));
QVERIFY(brightChar.has_value());
QCOMPARE(brightChar->foreground(), mpHost->mLightRed);
auto afterChar = charOfSegment(qsl("after"));
QVERIFY(afterChar.has_value());
QCOMPARE(afterChar->foreground(), mpHost->mFgColor);
QVERIFY(afterChar->isBold());
}
// SGR 39 and SGR 1 within a single sequence behave like the separate
// sequences.
void combinedDefaultForegroundResetAndBold()
{
injectData(QByteArrayLiteral("\x1b[31mred \x1b[39;1mbold"));
auto boldChar = charOfSegment(qsl("bold"));
QVERIFY(boldChar.has_value());
QCOMPARE(boldChar->foreground(), mpHost->mFgColor);
QVERIFY(boldChar->isBold());
}
// SGR 39 also clears a 256-color foreground set via SGR 38;5.
void boldAfterDefaultForegroundResetFrom256Color()
{
injectData(QByteArrayLiteral("\x1b[38;5;196mext \x1b[39mplain \x1b[1mbold"));
auto boldChar = charOfSegment(qsl("bold"));
QVERIFY(boldChar.has_value());
QCOMPARE(boldChar->foreground(), mpHost->mFgColor);
QVERIFY(boldChar->isBold());
}
// The background analog: SGR 49 restores the default background and a
// later bold does not disturb it.
void boldAfterDefaultBackgroundReset()
{
injectData(QByteArrayLiteral("\x1b[41mredbg \x1b[49mplainbg \x1b[1mboldbg"));
auto plainChar = charOfSegment(qsl("plainbg"));
QVERIFY(plainChar.has_value());
QCOMPARE(plainChar->background(), mpHost->mBgColor);
auto boldChar = charOfSegment(qsl("boldbg"));
QVERIFY(boldChar.has_value());
QCOMPARE(boldChar->background(), mpHost->mBgColor);
QCOMPARE(boldChar->foreground(), mpHost->mFgColor);
}
// Real bytes captured from eden-test.rpgframework.de:4000's "Check client
// compatibility" screen, where the bug was reported: a red checkbox dash
// reset with SGR 39, followed by a bold section header on the next line.
void realServerCompatibilityExcerpt()
{
injectData(QByteArrayLiteral("\x1b[1m 16 colors\x1b[22m: [\x1b[31m-\x1b[39m] \r\n"
"\x1b[4m\x1b[1mColor Capabilities \x1b[22m\x1b[24m"));
auto headerChar = charOfSegment(qsl("Color Capabilities"));
QVERIFY(headerChar.has_value());
QVERIFY(headerChar->foreground() != mpHost->mLightRed);
QCOMPARE(headerChar->foreground(), mpHost->mFgColor);
QVERIFY(headerChar->isBold());
QVERIFY(headerChar->isUnderlined());
}
void cleanupTestCase()
{
delete mpServer;
mpServer = nullptr;
mpHost = nullptr;
const QString path = mudlet::getMudletPath(enums::profileHomePath, mHostname);
QDir(path).removeRecursively();
delete mudlet::self();
}
};
void initializeQRCResourcesForSgrDefaultColorTest()
{
#ifdef INCLUDE_VARIABLE_SPLASH_SCREEN
qInitResources_additional_splash_screens();
#endif
#ifdef INCLUDE_FONTS
qInitResources_mudlet_fonts_common();
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
qInitResources_mudlet_fonts_posix();
#endif
#endif
qInitResources_mudlet();
qInitResources_qm();
}
#include "TelnetSgrDefaultColorTest.moc"
QTEST_MAIN(TelnetSgrDefaultColorTest)