remotedesktop 0.10.0__tar.gz → 0.12.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/PKG-INFO +1 -1
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/src/remotedesktop/__init__.py +1 -1
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/src/remotedesktop/client.py +4 -1
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/src/remotedesktop/performance.py +92 -16
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/src/remotedesktop/server.py +4 -1
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/src/remotedesktop/viewer.py +8 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/tests/test_input.py +19 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/tests/test_performance.py +43 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/.github/workflows/ci.yml +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/.gitignore +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/CLAUDE.md +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/LICENSE +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/pyproject.toml +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/readme.md +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/run_claude.bat +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/run_client.bat +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/run_server.bat +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/scripts/make_coverage_badge.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/scripts/make_venv.bat +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/src/remotedesktop/autostart.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/src/remotedesktop/clipboard.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/src/remotedesktop/config.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/src/remotedesktop/db.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/src/remotedesktop/discovery.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/src/remotedesktop/frames.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/src/remotedesktop/icon.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/src/remotedesktop/input_injection.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/src/remotedesktop/inventory.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/src/remotedesktop/logs.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/src/remotedesktop/modal_loop.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/src/remotedesktop/preferences.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/src/remotedesktop/protocol.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/src/remotedesktop/sharing.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/src/remotedesktop/tls.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/src/remotedesktop/window_state.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/tests/conftest.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/tests/test_autostart.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/tests/test_client_window.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/tests/test_clipboard.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/tests/test_config.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/tests/test_discovery.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/tests/test_frames.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/tests/test_icon.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/tests/test_input_injection.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/tests/test_inventory.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/tests/test_logs.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/tests/test_modal_loop.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/tests/test_preferences.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/tests/test_protocol.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/tests/test_server_window.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/tests/test_sharing.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/tests/test_smoke.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/tests/test_tls.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/tests/test_window_state.py +0 -0
- {remotedesktop-0.10.0 → remotedesktop-0.12.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remotedesktop
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.0
|
|
4
4
|
Summary: Remote desktop client/server for Windows computers on the same LAN, with autodiscovery. Provides screen, keyboard, mouse, and clipboard sharing without RDP or Microsoft authentication.
|
|
5
5
|
Author-email: James Abel <j@abel.co>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -110,7 +110,10 @@ class ClientWindow(QMainWindow):
|
|
|
110
110
|
InventoryTab(self.inventory, "Forget server", self._forget_server),
|
|
111
111
|
"Servers on LAN",
|
|
112
112
|
)
|
|
113
|
-
tabs.addTab(
|
|
113
|
+
tabs.addTab(
|
|
114
|
+
PerformanceTab(self.performance, local="client", remote="server"),
|
|
115
|
+
"Performance",
|
|
116
|
+
)
|
|
114
117
|
self.setCentralWidget(tabs)
|
|
115
118
|
|
|
116
119
|
self.discovery_panel = DiscoveryPanel(self)
|
|
@@ -30,13 +30,14 @@ All timing uses time.monotonic(), injectable as `clock=` for tests.
|
|
|
30
30
|
|
|
31
31
|
import itertools
|
|
32
32
|
import logging
|
|
33
|
+
import math
|
|
33
34
|
import time
|
|
34
35
|
from collections import deque
|
|
35
36
|
from collections.abc import Callable
|
|
36
37
|
from typing import Protocol
|
|
37
38
|
|
|
38
39
|
from PySide6.QtCore import QObject, QPointF, QTimer, Qt, Signal
|
|
39
|
-
from PySide6.QtGui import QColor, QPainter, QPaintEvent, QPolygonF, QShowEvent
|
|
40
|
+
from PySide6.QtGui import QColor, QPainter, QPaintEvent, QPen, QPolygonF, QShowEvent
|
|
40
41
|
from PySide6.QtWidgets import QVBoxLayout, QWidget
|
|
41
42
|
|
|
42
43
|
_log = logging.getLogger("remotedesktop.performance")
|
|
@@ -58,6 +59,7 @@ DEFAULT_INTERVAL_MS = 1000
|
|
|
58
59
|
DEFAULT_DEAD_AFTER_SECONDS = 10.0
|
|
59
60
|
_PING_PRUNE_SECONDS = 30.0
|
|
60
61
|
_HEARTBEAT_TICKS = 10 # one debug-log line per this many samples
|
|
62
|
+
_GRID_DIVISIONS = 4 # grid cells per axis, i.e. 5 ticks including both ends
|
|
61
63
|
|
|
62
64
|
|
|
63
65
|
class MetricSeries:
|
|
@@ -270,6 +272,30 @@ def format_ms(ms: float) -> str:
|
|
|
270
272
|
return f"{ms:.1f} ms"
|
|
271
273
|
|
|
272
274
|
|
|
275
|
+
def rate_unit(bytes_per_second: float) -> float:
|
|
276
|
+
"""The divisor format_rate will display this value in (B/KB/MB per s),
|
|
277
|
+
so axis ticks can be computed in the displayed unit and land on round
|
|
278
|
+
numbers there."""
|
|
279
|
+
if bytes_per_second >= 1024 * 1024:
|
|
280
|
+
return 1024.0 * 1024.0
|
|
281
|
+
if bytes_per_second >= 1024:
|
|
282
|
+
return 1024.0
|
|
283
|
+
return 1.0
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def nice_ceiling(value: float) -> float:
|
|
287
|
+
"""Smallest 1/2/5 x 10^k that is >= value, so axis ticks land on round
|
|
288
|
+
numbers instead of the raw data maximum."""
|
|
289
|
+
if value <= 0:
|
|
290
|
+
return 1.0
|
|
291
|
+
exponent = math.floor(math.log10(value))
|
|
292
|
+
for multiplier in (1.0, 2.0, 5.0, 10.0):
|
|
293
|
+
candidate = multiplier * 10.0**exponent
|
|
294
|
+
if candidate >= value:
|
|
295
|
+
return candidate
|
|
296
|
+
raise AssertionError("unreachable: 10 * 10**floor(log10(v)) >= v")
|
|
297
|
+
|
|
298
|
+
|
|
273
299
|
class GraphWidget(QWidget):
|
|
274
300
|
"""A rolling line graph over the monitor's window, painted with QPainter.
|
|
275
301
|
|
|
@@ -284,6 +310,7 @@ class GraphWidget(QWidget):
|
|
|
284
310
|
monitor: PerformanceMonitor,
|
|
285
311
|
format_value: Callable[[float], str],
|
|
286
312
|
*,
|
|
313
|
+
tick_unit: Callable[[float], float] = lambda _value: 1.0,
|
|
287
314
|
clock: Callable[[], float] = time.monotonic,
|
|
288
315
|
parent: QWidget | None = None,
|
|
289
316
|
) -> None:
|
|
@@ -292,8 +319,12 @@ class GraphWidget(QWidget):
|
|
|
292
319
|
self._series = series
|
|
293
320
|
self._monitor = monitor
|
|
294
321
|
self._format_value = format_value
|
|
322
|
+
# Maps the data maximum to the unit format_value displays it in, so
|
|
323
|
+
# the axis ceiling is a round number of that unit (100 KB/s, not the
|
|
324
|
+
# 97.7 KB/s that a round number of bytes/s would render as).
|
|
325
|
+
self._tick_unit = tick_unit
|
|
295
326
|
self._clock = clock
|
|
296
|
-
self.setMinimumSize(300,
|
|
327
|
+
self.setMinimumSize(300, 140) # room for the axis-label rows
|
|
297
328
|
|
|
298
329
|
def paintEvent(self, event: QPaintEvent) -> None:
|
|
299
330
|
painter = QPainter(self)
|
|
@@ -313,12 +344,52 @@ class GraphWidget(QWidget):
|
|
|
313
344
|
self.rect(), Qt.AlignmentFlag.AlignCenter, "no data"
|
|
314
345
|
)
|
|
315
346
|
return
|
|
316
|
-
|
|
347
|
+
raw_max = max(
|
|
317
348
|
(value for _l, _c, samples in data for _t, value in samples), default=1.0
|
|
318
349
|
)
|
|
319
|
-
|
|
350
|
+
unit = self._tick_unit(raw_max)
|
|
351
|
+
y_max = unit * nice_ceiling(raw_max / unit)
|
|
352
|
+
|
|
353
|
+
# Y tick labels (with units, via the formatter) size the left margin;
|
|
354
|
+
# the bottom leaves a row for X tick labels and one for the legend.
|
|
355
|
+
metrics = painter.fontMetrics()
|
|
356
|
+
y_ticks = [y_max * i / _GRID_DIVISIONS for i in range(_GRID_DIVISIONS + 1)]
|
|
357
|
+
y_labels = [self._format_value(v) for v in y_ticks]
|
|
358
|
+
left = 8 + max(metrics.horizontalAdvance(t) for t in y_labels) + 6
|
|
359
|
+
graph = self.rect().adjusted(left, 24, -12, -(2 * metrics.height() + 12))
|
|
360
|
+
|
|
361
|
+
grid_color = palette.color(palette.ColorRole.Mid)
|
|
362
|
+
grid_pen = QPen(grid_color, 0, Qt.PenStyle.DotLine)
|
|
363
|
+
text_color = palette.color(palette.ColorRole.Text)
|
|
364
|
+
for value, label in zip(y_ticks, y_labels):
|
|
365
|
+
y = graph.bottom() - graph.height() * (value / y_max)
|
|
366
|
+
painter.setPen(grid_pen)
|
|
367
|
+
painter.drawLine(graph.left(), round(y), graph.right(), round(y))
|
|
368
|
+
painter.setPen(text_color)
|
|
369
|
+
painter.drawText(
|
|
370
|
+
graph.left() - 6 - metrics.horizontalAdvance(label),
|
|
371
|
+
round(y) + metrics.ascent() // 2 - 1,
|
|
372
|
+
label,
|
|
373
|
+
)
|
|
374
|
+
x_label_baseline = graph.bottom() + metrics.ascent() + 4
|
|
375
|
+
for i in range(_GRID_DIVISIONS + 1):
|
|
376
|
+
seconds_ago = window * (1 - i / _GRID_DIVISIONS)
|
|
377
|
+
x = graph.left() + graph.width() * i / _GRID_DIVISIONS
|
|
378
|
+
painter.setPen(grid_pen)
|
|
379
|
+
painter.drawLine(round(x), graph.top(), round(x), graph.bottom())
|
|
380
|
+
label = f"{seconds_ago:g}"
|
|
381
|
+
width = metrics.horizontalAdvance(label)
|
|
382
|
+
# Center on the tick, but keep the edge labels inside the graph.
|
|
383
|
+
text_x = min(max(x - width / 2, graph.left()), graph.right() - width)
|
|
384
|
+
painter.setPen(text_color)
|
|
385
|
+
painter.drawText(round(text_x), x_label_baseline, label)
|
|
386
|
+
caption = "seconds ago"
|
|
387
|
+
painter.drawText(
|
|
388
|
+
self.rect().right() - 8 - metrics.horizontalAdvance(caption),
|
|
389
|
+
self.rect().bottom() - 6,
|
|
390
|
+
caption,
|
|
391
|
+
)
|
|
320
392
|
|
|
321
|
-
graph = self.rect().adjusted(8, 24, -8, -20)
|
|
322
393
|
for label, color, samples in data:
|
|
323
394
|
if not samples:
|
|
324
395
|
continue
|
|
@@ -336,14 +407,8 @@ class GraphWidget(QWidget):
|
|
|
336
407
|
latest = samples[-1][1] if samples else None
|
|
337
408
|
text = f"{label} {self._format_value(latest)}" if latest is not None else f"{label} —"
|
|
338
409
|
painter.setPen(color)
|
|
339
|
-
painter.drawText(x, self.rect().bottom() - 6, text)
|
|
340
|
-
x +=
|
|
341
|
-
painter.setPen(palette.color(palette.ColorRole.Text))
|
|
342
|
-
painter.drawText(
|
|
343
|
-
self.rect().right() - 8 - painter.fontMetrics().horizontalAdvance(self._format_value(y_max)),
|
|
344
|
-
16,
|
|
345
|
-
self._format_value(y_max),
|
|
346
|
-
)
|
|
410
|
+
painter.drawText(round(x), self.rect().bottom() - 6, text)
|
|
411
|
+
x += metrics.horizontalAdvance(text) + 16
|
|
347
412
|
|
|
348
413
|
|
|
349
414
|
class PerformanceTab(QWidget):
|
|
@@ -353,7 +418,14 @@ class PerformanceTab(QWidget):
|
|
|
353
418
|
monitor regardless, but a background tab schedules zero paint work.
|
|
354
419
|
"""
|
|
355
420
|
|
|
356
|
-
def __init__(
|
|
421
|
+
def __init__(
|
|
422
|
+
self,
|
|
423
|
+
monitor: PerformanceMonitor,
|
|
424
|
+
*,
|
|
425
|
+
local: str = "client",
|
|
426
|
+
remote: str = "server",
|
|
427
|
+
parent: QWidget | None = None,
|
|
428
|
+
) -> None:
|
|
357
429
|
super().__init__(parent)
|
|
358
430
|
self._monitor = monitor
|
|
359
431
|
self.bandwidth_graph = GraphWidget(
|
|
@@ -364,12 +436,16 @@ class PerformanceTab(QWidget):
|
|
|
364
436
|
],
|
|
365
437
|
monitor,
|
|
366
438
|
format_rate,
|
|
439
|
+
tick_unit=rate_unit,
|
|
367
440
|
)
|
|
441
|
+
# Each line is named for the side that sent the ping, so the same
|
|
442
|
+
# line has the same name in both apps ("client → server" is the
|
|
443
|
+
# client-measured round trip on either end).
|
|
368
444
|
self.ping_graph = GraphWidget(
|
|
369
445
|
"Round-trip time",
|
|
370
446
|
[
|
|
371
|
-
("
|
|
372
|
-
("
|
|
447
|
+
(f"{local} → {remote}", QColor("#ff9800"), monitor.rtt_ms),
|
|
448
|
+
(f"{remote} → {local}", QColor("#9c27b0"), monitor.peer_rtt_ms),
|
|
373
449
|
],
|
|
374
450
|
monitor,
|
|
375
451
|
format_ms,
|
|
@@ -108,7 +108,10 @@ class ServerWindow(QMainWindow):
|
|
|
108
108
|
InventoryTab(self.inventory, "Revoke access", self._revoke_client),
|
|
109
109
|
"Clients on LAN",
|
|
110
110
|
)
|
|
111
|
-
tabs.addTab(
|
|
111
|
+
tabs.addTab(
|
|
112
|
+
PerformanceTab(self.performance, local="server", remote="client"),
|
|
113
|
+
"Performance",
|
|
114
|
+
)
|
|
112
115
|
tabs.addTab(log_tab, "Connection log")
|
|
113
116
|
tabs.addTab(PreferencesTab(self._settings, self.performance), "Preferences")
|
|
114
117
|
self.setCentralWidget(tabs)
|
|
@@ -145,6 +145,14 @@ class ViewerWidget(QWidget):
|
|
|
145
145
|
self._pressed_keys.discard(vk)
|
|
146
146
|
self.inputEvent.emit({"action": "key", "vk": vk, "pressed": pressed})
|
|
147
147
|
|
|
148
|
+
def focusNextPrevChild(self, next: bool) -> bool:
|
|
149
|
+
# Qt consumes Tab/Shift+Tab for focus traversal before keyPressEvent
|
|
150
|
+
# ever runs; declining here makes them arrive as ordinary key events
|
|
151
|
+
# so they reach the remote desktop instead of moving local focus.
|
|
152
|
+
if self._frame is not None:
|
|
153
|
+
return False
|
|
154
|
+
return super().focusNextPrevChild(next)
|
|
155
|
+
|
|
148
156
|
def keyPressEvent(self, event: QKeyEvent) -> None:
|
|
149
157
|
self._key_event(event, True)
|
|
150
158
|
|
|
@@ -207,6 +207,25 @@ def test_viewer_forwards_wheel_and_key_release(qapp):
|
|
|
207
207
|
assert events[-1]["pressed"] is False
|
|
208
208
|
|
|
209
209
|
|
|
210
|
+
def test_tab_key_is_forwarded_not_used_for_focus_traversal(qapp):
|
|
211
|
+
viewer = ViewerWidget()
|
|
212
|
+
viewer.resize(400, 400)
|
|
213
|
+
viewer.show_frame(QImage(200, 100, QImage.Format.Format_RGB32))
|
|
214
|
+
events: list[dict] = []
|
|
215
|
+
viewer.inputEvent.connect(events.append)
|
|
216
|
+
# Send Tab through the real event path: QWidget.event() offers it to
|
|
217
|
+
# focusNextPrevChild before keyPressEvent, which is what ate Tab.
|
|
218
|
+
tab = QKeyEvent(QEvent.Type.KeyPress, Qt.Key.Key_Tab, Qt.KeyboardModifier.NoModifier, 0, 0x09, 0)
|
|
219
|
+
qapp.sendEvent(viewer, tab)
|
|
220
|
+
assert events == [{"action": "key", "vk": 0x09, "pressed": True}]
|
|
221
|
+
# Shift+Tab arrives as Backtab and must be forwarded the same way.
|
|
222
|
+
backtab = QKeyEvent(
|
|
223
|
+
QEvent.Type.KeyPress, Qt.Key.Key_Backtab, Qt.KeyboardModifier.ShiftModifier, 0, 0x09, 0
|
|
224
|
+
)
|
|
225
|
+
qapp.sendEvent(viewer, backtab)
|
|
226
|
+
assert events[-1] == {"action": "key", "vk": 0x09, "pressed": True}
|
|
227
|
+
|
|
228
|
+
|
|
210
229
|
def test_release_when_frame_vanished_mid_drag(qapp):
|
|
211
230
|
viewer = ViewerWidget()
|
|
212
231
|
viewer.resize(400, 400)
|
|
@@ -12,6 +12,8 @@ from remotedesktop.performance import (
|
|
|
12
12
|
MetricSeries,
|
|
13
13
|
PerformanceMonitor,
|
|
14
14
|
PerformanceTab,
|
|
15
|
+
nice_ceiling,
|
|
16
|
+
rate_unit,
|
|
15
17
|
)
|
|
16
18
|
from remotedesktop.protocol import MessageStream
|
|
17
19
|
from remotedesktop.sharing import ShareClient, ShareServer
|
|
@@ -325,11 +327,52 @@ def test_share_client_drops_the_connection_on_lost_signal(qapp, tmp_path):
|
|
|
325
327
|
assert any("Connection lost" in s and "10 s" in s for s in statuses)
|
|
326
328
|
|
|
327
329
|
|
|
330
|
+
def test_nice_ceiling_rounds_up_to_1_2_5():
|
|
331
|
+
assert nice_ceiling(0.0) == 1.0
|
|
332
|
+
assert nice_ceiling(-3.0) == 1.0
|
|
333
|
+
assert nice_ceiling(0.7) == 1.0
|
|
334
|
+
assert nice_ceiling(1.0) == 1.0
|
|
335
|
+
assert nice_ceiling(1.5) == 2.0
|
|
336
|
+
assert nice_ceiling(3.0) == 5.0
|
|
337
|
+
assert nice_ceiling(42.0) == 50.0
|
|
338
|
+
assert nice_ceiling(50.0) == 50.0
|
|
339
|
+
assert nice_ceiling(99.0) == 100.0
|
|
340
|
+
assert nice_ceiling(2049.0) == 5000.0
|
|
341
|
+
assert nice_ceiling(0.03) == pytest.approx(0.05)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def test_rate_unit_matches_format_rate_unit():
|
|
345
|
+
assert rate_unit(500.0) == 1.0
|
|
346
|
+
assert rate_unit(2048.0) == 1024.0
|
|
347
|
+
assert rate_unit(3 * 1024 * 1024) == 1024.0 * 1024.0
|
|
348
|
+
# A round tick count in the displayed unit: 70000 B/s displays in KB/s,
|
|
349
|
+
# and the resulting ceiling is 100 KB/s exactly.
|
|
350
|
+
unit = rate_unit(70000.0)
|
|
351
|
+
assert unit * nice_ceiling(70000.0 / unit) == 100.0 * 1024.0
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def test_rtt_lines_are_named_for_the_side_that_pings(qapp):
|
|
355
|
+
monitor = PerformanceMonitor()
|
|
356
|
+
client_tab = PerformanceTab(monitor, local="client", remote="server")
|
|
357
|
+
server_tab = PerformanceTab(monitor, local="server", remote="client")
|
|
358
|
+
# This side's own measurement (rtt_ms) is listed first; the same physical
|
|
359
|
+
# measurement carries the same name in both apps.
|
|
360
|
+
assert [label for label, _c, series in client_tab.ping_graph._series] == [
|
|
361
|
+
"client → server",
|
|
362
|
+
"server → client",
|
|
363
|
+
]
|
|
364
|
+
assert [label for label, _c, series in server_tab.ping_graph._series] == [
|
|
365
|
+
"server → client",
|
|
366
|
+
"client → server",
|
|
367
|
+
]
|
|
368
|
+
|
|
369
|
+
|
|
328
370
|
def test_graph_widgets_render_headless(qapp):
|
|
329
371
|
seeded = PerformanceMonitor()
|
|
330
372
|
seeded.send_bps.add(100.0)
|
|
331
373
|
seeded.send_bps.add(2048.0)
|
|
332
374
|
seeded.recv_bps.add(50.0)
|
|
375
|
+
seeded.rtt_ms.add(12.5) # exercises the RTT graph's grid/axes path too
|
|
333
376
|
for monitor in (seeded, PerformanceMonitor()): # data and "no data" paths
|
|
334
377
|
tab = PerformanceTab(monitor)
|
|
335
378
|
tab.resize(400, 300)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|