#include #include "moonlight_session.h" using namespace vdi; class MoonlightSessionTest : public QObject { Q_OBJECT private slots: void parsesSplitReadyMarkers(); void requiresBothReadyMarkers(); void acceptsSeparateHardwareTestDecoder(); void rejectsSecondStart(); void reportsKioskLaunchFailure(); void buildsConsistentStreamArguments(); void selectsProfileWithoutLosingStats(); void rejectsInvalidDecoderOverride(); void validatesConfiguredBitrate(); }; void MoonlightSessionTest::validatesConfiguredBitrate() { ConnectionTicket ticket; ticket.width = 2560; ticket.height = 1440; QProcessEnvironment environment; for (const QString& value : {QString("25000"), QString("0"), QString("150001"), QString("invalid")}) { environment.insert("VDI_MOONLIGHT_BITRATE_KBPS", value); auto options = MoonlightSession::optionsFromEnvironment(environment); const auto arguments = MoonlightSession::buildStreamArguments(ticket, options); QCOMPARE(arguments.at(arguments.indexOf("--bitrate") + 1), value == "25000" ? QString("25000") : QString("30000")); } } void MoonlightSessionTest::parsesSplitReadyMarkers() { MoonlightOutputParser parser; parser.append("Test decode succ"); QVERIFY(!parser.isReady()); parser.append("essful\nRenderer 'SDL' with 'DRM' backend "); QVERIFY(parser.hasTestDecode()); QVERIFY(!parser.hasRenderer()); parser.append("chosen\n"); QVERIFY(parser.isReady()); } void MoonlightSessionTest::requiresBothReadyMarkers() { MoonlightOutputParser parser; parser.append("Test decode successful\n"); QVERIFY(!parser.isReady()); parser.reset(); parser.append("Renderer 'SDL' chosen\n"); QVERIFY(!parser.isReady()); parser.append("Connection terminated: -1\n"); QVERIFY(parser.hasTerminalError()); QVERIFY(!parser.isReady()); } void MoonlightSessionTest::acceptsSeparateHardwareTestDecoder() { MoonlightOutputParser parser; parser.append("Using custom H.264 decoder (H264_DECODER_HINT): h264_rkmpp\n"); QVERIFY(!parser.isReady()); parser.append("Not reusing test decoder for h264_rkmpp\n"); QVERIFY(parser.hasTestDecode()); QVERIFY(!parser.isReady()); parser.append("Renderer 'EGL' with 'DRM' backend chosen\n"); QVERIFY(parser.isReady()); parser.append("Connection terminated: -1\n"); QVERIFY(!parser.isReady()); } void MoonlightSessionTest::rejectsSecondStart() { QTemporaryDir tempDir; QVERIFY(tempDir.isValid()); const QString executable = tempDir.filePath(QStringLiteral("moonlight-qt")); QFile script(executable); QVERIFY(script.open(QIODevice::WriteOnly | QIODevice::Text)); script.write("#!/bin/sh\n" "printf 'Test decode successful\\n'\n" "printf \"Renderer 'SDL' with 'DRM' backend chosen\\n\"\n" "sleep 5\n"); script.close(); QVERIFY(script.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner | QFileDevice::ReadGroup | QFileDevice::ExeGroup | QFileDevice::ReadOther | QFileDevice::ExeOther)); const QByteArray oldPath = qgetenv("PATH"); qputenv("PATH", tempDir.path().toUtf8() + ':' + oldPath); ConnectionTicket ticket; ticket.endpoint = QStringLiteral("127.0.0.1"); ticket.app = QStringLiteral("Desktop"); MoonlightSession session; int started = 0; int ready = 0; int terminal = 0; const auto callback = [&](const MoonlightSession::Event& event) { if (event.type == MoonlightSession::EventType::kStarted) { ++started; } else if (event.type == MoonlightSession::EventType::kStreamReady) { ++ready; } else if (event.type == MoonlightSession::EventType::kFailed || event.type == MoonlightSession::EventType::kFinished) { ++terminal; } }; QVERIFY(session.start(ticket, callback)); QTRY_COMPARE_WITH_TIMEOUT(started, 1, 2000); QVERIFY(!session.start(ticket, callback)); QTRY_COMPARE_WITH_TIMEOUT(ready, 1, 3000); session.stop(); QTRY_COMPARE_WITH_TIMEOUT(terminal, 1, 4000); QCOMPARE(ready, 1); QCOMPARE(terminal, 1); qputenv("PATH", oldPath); } void MoonlightSessionTest::reportsKioskLaunchFailure() { QTemporaryDir tempDir; QVERIFY(tempDir.isValid()); const QString executable = tempDir.filePath(QStringLiteral("failed-client")); QFile script(executable); QVERIFY(script.open(QIODevice::WriteOnly | QIODevice::Text)); script.write("#!/bin/sh\n" "test \"$VDI_KIOSK_STREAM\" = 1 || exit 2\n" "exit 1\n"); script.close(); QVERIFY(script.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner)); MoonlightSession session; MoonlightSession::Options options; options.program = executable; session.setOptions(options); ConnectionTicket ticket; ticket.endpoint = QStringLiteral("127.0.0.1"); ticket.app = QStringLiteral("MissingApplication"); int failed = 0; int finished = 0; QString message; QVERIFY(session.start(ticket, [&](const MoonlightSession::Event& event) { if (event.type == MoonlightSession::EventType::kFailed) { ++failed; message = event.message; } else if (event.type == MoonlightSession::EventType::kFinished) { ++finished; } })); QTRY_COMPARE_WITH_TIMEOUT(failed, 1, 2000); QCOMPARE(finished, 0); QVERIFY(message.contains(QStringLiteral("code=1"))); } void MoonlightSessionTest::buildsConsistentStreamArguments() { ConnectionTicket ticket; ticket.endpoint = QStringLiteral("192.168.1.70"); ticket.app = QStringLiteral("Desktop"); ticket.width = 2560; ticket.height = 1440; ticket.fps = 30; MoonlightSession::Options options; options.profile = QStringLiteral("low-latency"); options.decoder = QStringLiteral("software"); options.videoCodec = QStringLiteral("H.264"); options.vsync = false; options.framePacing = false; options.performanceOverlay = true; const QStringList args = MoonlightSession::buildStreamArguments(ticket, options); QVERIFY(args.contains(QStringLiteral("--resolution"))); QVERIFY(args.contains(QStringLiteral("2560x1440"))); QVERIFY(args.contains(QStringLiteral("--fps"))); QVERIFY(args.contains(QStringLiteral("30"))); QVERIFY(args.contains(QStringLiteral("--bitrate"))); QVERIFY(args.contains(QStringLiteral("30000"))); QVERIFY(args.contains(QStringLiteral("--no-vsync"))); QVERIFY(args.contains(QStringLiteral("--no-frame-pacing"))); QVERIFY(args.contains(QStringLiteral("--performance-overlay"))); QVERIFY(args.contains(QStringLiteral("--absolute-mouse"))); const int captureOption = args.indexOf(QStringLiteral("--capture-system-keys")); QVERIFY(captureOption >= 0); QCOMPARE(args.value(captureOption + 1), QStringLiteral("always")); } void MoonlightSessionTest::selectsProfileWithoutLosingStats() { QProcessEnvironment environment; environment.insert(QStringLiteral("VDI_STREAM_PROFILE"), QStringLiteral("low-latency")); environment.insert(QStringLiteral("VDI_STREAM_STATS"), QStringLiteral("1")); auto options = MoonlightSession::optionsFromEnvironment(environment); QVERIFY(!options.vsync); QVERIFY(!options.framePacing); QVERIFY(options.performanceOverlay); environment.insert(QStringLiteral("VDI_STREAM_PROFILE"), QStringLiteral("smooth")); options = MoonlightSession::optionsFromEnvironment(environment); QVERIFY(options.vsync); QVERIFY(options.framePacing); QVERIFY(options.performanceOverlay); } void MoonlightSessionTest::rejectsInvalidDecoderOverride() { QProcessEnvironment environment; environment.insert(QStringLiteral("VDI_MOONLIGHT_DECODER"), QStringLiteral("h264_rkmpp")); environment.insert(QStringLiteral("VDI_STREAM_PROFILE"), QStringLiteral("invalid")); auto options = MoonlightSession::optionsFromEnvironment(environment); QCOMPARE(options.decoder, QStringLiteral("software")); QCOMPARE(options.profile, QStringLiteral("smooth")); environment.insert(QStringLiteral("VDI_MOONLIGHT_DECODER"), QStringLiteral("hardware")); options = MoonlightSession::optionsFromEnvironment(environment); QCOMPARE(options.decoder, QStringLiteral("hardware")); } QTEST_GUILESS_MAIN(MoonlightSessionTest) #include "test_moonlight_session.moc"