From fae44ecd9e3342d81a7198a48e2ea64a44ee7584 Mon Sep 17 00:00:00 2001 From: reionwong Date: Fri, 25 Sep 2026 22:51:39 +0800 Subject: [PATCH] test(qmltermwidget): test key encoding and escape sequence parsing --- CMakeLists.txt | 7 ++ qmltermwidget/CMakeLists.txt | 19 ++- tests/CMakeLists.txt | 8 ++ tests/vt102emulation_test.cpp | 224 ++++++++++++++++++++++++++++++++++ 4 files changed, 254 insertions(+), 4 deletions(-) create mode 100644 tests/CMakeLists.txt create mode 100644 tests/vt102emulation_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d3ab689..7eab942 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,8 @@ set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) +include(CTest) + set(QT Core Gui Network Qml Quick Widgets DBus LinguistTools) find_package(Qt6 REQUIRED ${QT}) find_package(CutefishFramework REQUIRED COMPONENTS Application) @@ -39,6 +41,11 @@ set(RESOURCES ) add_subdirectory(qmltermwidget) + +if(BUILD_TESTING) + add_subdirectory(tests) +endif() + add_executable(${PROJECT_NAME} ${SRCS} ${RESOURCES}) target_link_libraries(${PROJECT_NAME} Qt6::Core diff --git a/qmltermwidget/CMakeLists.txt b/qmltermwidget/CMakeLists.txt index d763d25..0287214 100644 --- a/qmltermwidget/CMakeLists.txt +++ b/qmltermwidget/CMakeLists.txt @@ -59,15 +59,17 @@ set(TERMWIDGET_SRCS lib/TerminalDisplay.cpp lib/tools.cpp lib/Vt102Emulation.cpp - src/qmltermwidget_plugin.h - src/qmltermwidget_plugin.cpp ) -add_library(cutefishqmltermwidget MODULE +# The plugin's objects, which the tests link as well. +add_library(qmltermwidget_core OBJECT ${TERMWIDGET_SRCS} ) -target_link_libraries(cutefishqmltermwidget +set_target_properties(qmltermwidget_core PROPERTIES POSITION_INDEPENDENT_CODE ON) +target_include_directories(qmltermwidget_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/lib) + +target_link_libraries(qmltermwidget_core PUBLIC Qt6::Core Qt6::Gui Qt6::Network @@ -77,6 +79,15 @@ target_link_libraries(cutefishqmltermwidget Cutefish::Application ) +add_library(cutefishqmltermwidget MODULE + src/qmltermwidget_plugin.h + src/qmltermwidget_plugin.cpp +) + +target_link_libraries(cutefishqmltermwidget + qmltermwidget_core +) + install(TARGETS cutefishqmltermwidget DESTINATION ${INSTALL_QMLDIR}/Cutefish/TermWidget) install(FILES src/qmldir DESTINATION ${INSTALL_QMLDIR}/Cutefish/TermWidget) install(FILES src/QMLTermScrollbar.qml DESTINATION ${INSTALL_QMLDIR}/Cutefish/TermWidget) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..99fe9ec --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,8 @@ +find_package(Qt6 REQUIRED Test) + +add_executable(vt102emulation-test vt102emulation_test.cpp) +target_link_libraries(vt102emulation-test PRIVATE qmltermwidget_core Qt6::Test) +target_compile_definitions(vt102emulation-test PRIVATE + KB_LAYOUTS_DIR="${CMAKE_SOURCE_DIR}/qmltermwidget/lib/kb-layouts" +) +add_test(NAME vt102emulation COMMAND vt102emulation-test -platform offscreen) diff --git a/tests/vt102emulation_test.cpp b/tests/vt102emulation_test.cpp new file mode 100644 index 0000000..bdd9c6a --- /dev/null +++ b/tests/vt102emulation_test.cpp @@ -0,0 +1,224 @@ +#include + +#include +#include + +#include "KeyboardTranslator.h" +#include "ScreenWindow.h" +#include "Vt102Emulation.h" + +using namespace Konsole; + +namespace { + +// Stands in for the pty: the program's output goes in, and what the terminal +// answers or types comes out. +class Terminal +{ +public: + Terminal() + : m_window(m_emulation.createWindow()) + { + m_emulation.setKeyBindings(QString()); + QObject::connect(&m_emulation, &Emulation::sendData, [this](const char *data, int length) { + m_sent.append(data, length); + }); + QObject::connect(&m_emulation, &Emulation::handleCommandFromKeyboard, + [this](KeyboardTranslator::Command command) { + m_commands.append(command); + }); + } + + QByteArray print(const QByteArray &output) + { + m_emulation.receiveData(output.constData(), output.size()); + return std::exchange(m_sent, {}); + } + + QByteArray press(int key, Qt::KeyboardModifiers modifiers = Qt::NoModifier, + const QString &text = QString()) + { + QKeyEvent event(QEvent::KeyPress, key, modifiers, text); + m_emulation.sendKeyEvent(&event, false); + return std::exchange(m_sent, {}); + } + + // A cell of the screen's first line. + Character cell(int column) + { + const Screen *screen = m_window->screen(); + QVector line(screen->getColumns()); + screen->getImage(line.data(), line.size(), screen->getHistLines(), screen->getHistLines()); + return line.at(column); + } + + QString line(int length) + { + QString text; + for (int column = 0; column < length; ++column) + text += QChar(char16_t(cell(column).character)); + return text; + } + + QList takeCommands() { return std::exchange(m_commands, {}); } + +private: + Vt102Emulation m_emulation; + ScreenWindow *m_window; + QByteArray m_sent; + QList m_commands; +}; + +} // namespace + +class Vt102EmulationTest : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase() + { + qputenv("KB_LAYOUT_DIR", KB_LAYOUTS_DIR); + } + + void keysStayAsTheyWereUntilAProgramAsks() + { + Terminal terminal; + QCOMPARE(terminal.print("\033[?u"), QByteArray("\033[?0u")); + QCOMPARE(terminal.press(Qt::Key_Return, Qt::ShiftModifier, QStringLiteral("\r")), + QByteArray("\033OM")); + QCOMPARE(terminal.press(Qt::Key_A, Qt::ControlModifier, QStringLiteral("\x01")), + QByteArray("\x01")); + } + + void flagsArePushedPoppedAndSet() + { + Terminal terminal; + // Only disambiguation is supported, so the query reports it alone. + terminal.print("\033[>5u"); + QCOMPARE(terminal.print("\033[?u"), QByteArray("\033[?1u")); + terminal.print("\033[>0u"); + QCOMPARE(terminal.print("\033[?u"), QByteArray("\033[?0u")); + terminal.print("\033[1u\033[?1049h"); + QCOMPARE(terminal.print("\033[?u"), QByteArray("\033[?0u")); + terminal.print("\033[?1049l"); + QCOMPARE(terminal.print("\033[?u"), QByteArray("\033[?1u")); + } + + void keysAreDisambiguated_data() + { + QTest::addColumn("key"); + QTest::addColumn("modifiers"); + QTest::addColumn("text"); + QTest::addColumn("sent"); + + const Qt::KeyboardModifiers none; + const Qt::KeyboardModifiers shift = Qt::ShiftModifier; + const Qt::KeyboardModifiers ctrl = Qt::ControlModifier; + const Qt::KeyboardModifiers alt = Qt::AltModifier; + const Qt::KeyboardModifiers keypad = Qt::KeypadModifier; + + QTest::newRow("enter") << int(Qt::Key_Return) << none << "\r" << QByteArray("\r"); + QTest::newRow("shift+enter") << int(Qt::Key_Return) << shift << "\r" << QByteArray("\033[13;2u"); + QTest::newRow("ctrl+enter") << int(Qt::Key_Return) << ctrl << "\r" << QByteArray("\033[13;5u"); + QTest::newRow("tab") << int(Qt::Key_Tab) << none << "\t" << QByteArray("\t"); + QTest::newRow("shift+tab") << int(Qt::Key_Backtab) << shift << "" << QByteArray("\033[9;2u"); + QTest::newRow("backspace") << int(Qt::Key_Backspace) << none << "\b" << QByteArray("\x7f"); + QTest::newRow("alt+backspace") << int(Qt::Key_Backspace) << alt << "\b" << QByteArray("\033[127;3u"); + QTest::newRow("escape") << int(Qt::Key_Escape) << none << "\x1b" << QByteArray("\033[27u"); + QTest::newRow("a") << int(Qt::Key_A) << none << "a" << QByteArray("a"); + QTest::newRow("shift+a") << int(Qt::Key_A) << shift << "A" << QByteArray("A"); + QTest::newRow("ctrl+a") << int(Qt::Key_A) << ctrl << "\x01" << QByteArray("\033[97;5u"); + QTest::newRow("ctrl+shift+c") << int(Qt::Key_C) << (ctrl | shift) << "\x03" << QByteArray("\033[99;6u"); + QTest::newRow("alt+b") << int(Qt::Key_B) << alt << "b" << QByteArray("\033[98;3u"); + QTest::newRow("ctrl+shift+1") << int(Qt::Key_Exclam) << (ctrl | shift) << "!" << QByteArray("\033[49;6u"); + QTest::newRow("ctrl+space") << int(Qt::Key_Space) << ctrl << " " << QByteArray("\033[32;5u"); + QTest::newRow("ctrl+cyrillic") << 0x0424 << ctrl << "ф" << QByteArray("\033[1092;5u"); + QTest::newRow("altgr text") << int(Qt::Key_At) << Qt::KeyboardModifiers(Qt::GroupSwitchModifier) << "@" + << QByteArray("@"); + QTest::newRow("up") << int(Qt::Key_Up) << none << "" << QByteArray("\033[A"); + QTest::newRow("ctrl+up") << int(Qt::Key_Up) << ctrl << "" << QByteArray("\033[1;5A"); + QTest::newRow("f1") << int(Qt::Key_F1) << none << "" << QByteArray("\033[P"); + QTest::newRow("shift+f1") << int(Qt::Key_F1) << shift << "" << QByteArray("\033[1;2P"); + QTest::newRow("f3") << int(Qt::Key_F3) << none << "" << QByteArray("\033[13~"); + QTest::newRow("f13") << int(Qt::Key_F13) << none << "" << QByteArray("\033[57376u"); + QTest::newRow("ctrl+page down") << int(Qt::Key_PageDown) << ctrl << "" << QByteArray("\033[6;5~"); + QTest::newRow("delete") << int(Qt::Key_Delete) << none << "\x7f" << QByteArray("\033[3~"); + QTest::newRow("keypad enter") << int(Qt::Key_Enter) << keypad << "\r" << QByteArray("\033[57414u"); + QTest::newRow("keypad 1") << int(Qt::Key_1) << keypad << "1" << QByteArray("1"); + QTest::newRow("ctrl+keypad 1") << int(Qt::Key_1) << (keypad | ctrl) << "1" << QByteArray("\033[57400;5u"); + } + + void keysAreDisambiguated() + { + QFETCH(int, key); + QFETCH(Qt::KeyboardModifiers, modifiers); + QFETCH(QString, text); + QFETCH(QByteArray, sent); + + Terminal terminal; + terminal.print("\033[>1u"); + QCOMPARE(terminal.press(key, modifiers, text), sent); + } + + void pastedTextGoesOutAsItIs() + { + Terminal terminal; + terminal.print("\033[>1u"); + QCOMPARE(terminal.press(0, Qt::NoModifier, QStringLiteral("ls -l\r")), QByteArray("ls -l\r")); + } + + void scrollingKeysStillScroll() + { + Terminal terminal; + terminal.print("\033[>1u"); + QCOMPARE(terminal.press(Qt::Key_PageUp, Qt::ShiftModifier), QByteArray()); + const QList commands = terminal.takeCommands(); + QCOMPARE(commands.size(), 1); + QVERIFY(commands.first() == KeyboardTranslator::ScrollPageUpCommand); + } + + void prefixedSequencesPrintNothing() + { + Terminal terminal; + terminal.print("a\033[=5;1ub\033[4;2md"); + QCOMPARE(terminal.line(4), QStringLiteral("abcd")); + } + + void subParametersStayWithTheirAttribute() + { + Terminal terminal; + terminal.print("\033[4:3mA\033[4:0mB\033[58;5;1mC\033[38:2::255:0:0mD\033[m"); + QCOMPARE(terminal.line(4), QStringLiteral("ABCD")); + + QVERIFY(terminal.cell(0).rendition & RE_UNDERLINE); + QVERIFY(!(terminal.cell(0).rendition & RE_ITALIC)); + QVERIFY(!(terminal.cell(1).rendition & RE_UNDERLINE)); + // An underline colour sets no other attribute. + QCOMPARE(terminal.cell(2).rendition, terminal.cell(1).rendition); + QVERIFY(terminal.cell(3).foregroundColor == CharacterColor(COLOR_SPACE_RGB, 0xff0000)); + } +}; + +QTEST_MAIN(Vt102EmulationTest) + +#include "vt102emulation_test.moc"