功能:完善2K串流、显示恢复与输入优化
This commit is contained in:
1 parent
e47b01beec
commit
938ec97cce
95 files changed
+5788
-72
No files matched your search
@@ -0,0 +1,140 @@
|
||||
"""Failure and hotplug scenarios; no real display or sleep required."""
|
||||
|
||||
from pathlib import Path
|
||||
import runpy
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
guard_module = runpy.run_path(str(Path(__file__).resolve().parents[1] / "scripts/vdi-display-guard"))
|
||||
Guard = guard_module["Guard"]
|
||||
MODE = guard_module["MODE"]
|
||||
|
||||
|
||||
class FakeRandR:
|
||||
def __init__(self):
|
||||
self.connected = True
|
||||
self.defined = True
|
||||
self.attached = True
|
||||
self.active = "1920x1440"
|
||||
self.writes = []
|
||||
self.fail_apply = False
|
||||
self.query_error = False
|
||||
self.keep_wrong_mode = False
|
||||
|
||||
def __call__(self, *args):
|
||||
if args == ("--current",):
|
||||
if self.query_error:
|
||||
raise RuntimeError("Can't open display :0")
|
||||
status = "connected primary " + ("2560x1440+0+0" if self.active == MODE else "1920x1440+0+0")
|
||||
if not self.connected:
|
||||
status = "disconnected"
|
||||
query = "Screen 0: current 1920 x 1440\nHDMI-1 " + status + " (normal)\n"
|
||||
if self.connected:
|
||||
query += " 1920x1440 75.00" + ("*" if self.active != MODE else "") + "\n"
|
||||
if self.attached:
|
||||
query += " " + MODE + " 60.00" + ("*" if self.active == MODE else "") + "\n"
|
||||
if self.defined and (not self.attached or not self.connected):
|
||||
query += " " + MODE + " (0x189) 297.000MHz +HSync -VSync\n"
|
||||
query += " h: width 2560 start 2608\n v: height 1440 start 1443\n"
|
||||
return query
|
||||
self.writes.append(args)
|
||||
if args[0] == "--newmode":
|
||||
if self.defined:
|
||||
raise RuntimeError("BadName: duplicate mode")
|
||||
self.defined = True
|
||||
elif args[0] == "--addmode":
|
||||
assert self.defined
|
||||
self.attached = True
|
||||
elif args[0] == "--output":
|
||||
if self.fail_apply:
|
||||
raise RuntimeError("Configure crtc failed")
|
||||
assert self.attached
|
||||
if not self.keep_wrong_mode:
|
||||
self.active = MODE
|
||||
return ""
|
||||
|
||||
|
||||
class DisplayGuardTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.xr = FakeRandR()
|
||||
self.logs = []
|
||||
self.guard = Guard(self.xr, self.logs.append)
|
||||
|
||||
def tick(self, now):
|
||||
with patch("time.monotonic", return_value=now):
|
||||
return self.guard.step(now)
|
||||
|
||||
def test_mode_drift_restores_after_settling(self):
|
||||
self.assertFalse(self.tick(0))
|
||||
self.assertEqual(self.xr.writes, [])
|
||||
self.assertTrue(self.tick(2))
|
||||
self.assertEqual(len(self.xr.writes), 1)
|
||||
self.assertEqual(self.xr.active, MODE)
|
||||
|
||||
def test_healthy_display_does_not_modeset_or_repeat_logs(self):
|
||||
self.xr.active = MODE
|
||||
for now in range(0, 100, 2):
|
||||
self.assertTrue(self.tick(now))
|
||||
self.assertEqual(self.xr.writes, [])
|
||||
self.assertEqual(len(self.logs), 1)
|
||||
|
||||
def test_disconnected_never_writes_and_reconnect_settles(self):
|
||||
self.tick(0)
|
||||
self.xr.connected = False
|
||||
for now in range(2, 20, 2):
|
||||
self.assertFalse(self.tick(now))
|
||||
self.assertEqual(self.xr.writes, [])
|
||||
self.xr.connected = True
|
||||
self.assertFalse(self.tick(20))
|
||||
self.assertTrue(self.tick(22))
|
||||
|
||||
def test_new_x_server_recreates_and_attaches_mode(self):
|
||||
self.xr.defined = self.xr.attached = False
|
||||
self.tick(0)
|
||||
self.assertTrue(self.tick(2))
|
||||
self.assertEqual([w[0] for w in self.xr.writes], ["--newmode", "--addmode", "--output"])
|
||||
|
||||
def test_hotplug_detached_mode_is_not_created_twice(self):
|
||||
self.xr.attached = False
|
||||
self.tick(0)
|
||||
self.assertTrue(self.tick(2))
|
||||
self.assertEqual([w[0] for w in self.xr.writes], ["--addmode", "--output"])
|
||||
|
||||
def test_late_desktop_settings_overwrite_is_corrected_again(self):
|
||||
self.tick(0)
|
||||
self.assertTrue(self.tick(2))
|
||||
self.xr.active = "1920x1440"
|
||||
self.assertFalse(self.tick(4))
|
||||
self.assertTrue(self.tick(6))
|
||||
self.assertEqual(len(self.xr.writes), 2)
|
||||
|
||||
def test_modeset_failure_backs_off_and_eventually_recovers(self):
|
||||
self.xr.fail_apply = True
|
||||
self.tick(0)
|
||||
self.tick(2) # retry at 4
|
||||
self.tick(4) # retry at 8
|
||||
self.tick(6) # no write
|
||||
self.assertEqual(len(self.xr.writes), 2)
|
||||
self.xr.fail_apply = False
|
||||
self.assertTrue(self.tick(8))
|
||||
self.assertEqual(len(self.xr.writes), 3)
|
||||
|
||||
def test_apply_success_must_be_verified(self):
|
||||
self.xr.keep_wrong_mode = True
|
||||
self.tick(0)
|
||||
self.assertFalse(self.tick(2))
|
||||
self.assertIn("not retained", self.logs[-1])
|
||||
|
||||
def test_x11_unavailable_does_not_write_and_then_recovers(self):
|
||||
self.xr.query_error = True
|
||||
for now in range(0, 20, 2):
|
||||
self.assertFalse(self.tick(now))
|
||||
self.assertEqual(self.xr.writes, [])
|
||||
self.assertEqual(len(self.logs), 1)
|
||||
self.xr.query_error = False
|
||||
self.assertFalse(self.tick(20))
|
||||
self.assertTrue(self.tick(22))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -10,7 +10,11 @@ class MoonlightSessionTest : public QObject {
|
||||
private slots:
|
||||
void parsesSplitReadyMarkers();
|
||||
void requiresBothReadyMarkers();
|
||||
void acceptsSeparateHardwareTestDecoder();
|
||||
void rejectsSecondStart();
|
||||
void buildsConsistentStreamArguments();
|
||||
void selectsProfileWithoutLosingStats();
|
||||
void rejectsInvalidDecoderOverride();
|
||||
};
|
||||
|
||||
void MoonlightSessionTest::parsesSplitReadyMarkers() {
|
||||
@@ -38,6 +42,19 @@ void MoonlightSessionTest::requiresBothReadyMarkers() {
|
||||
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());
|
||||
@@ -90,5 +107,61 @@ void MoonlightSessionTest::rejectsSecondStart() {
|
||||
qputenv("PATH", oldPath);
|
||||
}
|
||||
|
||||
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")));
|
||||
}
|
||||
|
||||
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"
|
||||
Reference in new issue
Block a user