1
0
mirror of https://github.com/kellyjonbrazil/jc.git synced 2025-07-13 01:20:24 +02:00

[xrandr] Fix 453 devices issue (#455)

* [xrandr] Fix bug 453, clean up data model

* Fix: 'devices' was originally not a list, just assigned each time it
was parsed. Made that a list and appended to it.
* Removed distinction between unassociated/associated devices
* Added test for @marcin-koziol's problem
* Put tests into separate test methods

* Formatting cleanup

* Backwards compatible type syntax

---------

Co-authored-by: Kelly Brazil <kellyjonbrazil@gmail.com>
This commit is contained in:
Kevin Lyter
2023-09-30 15:19:14 -07:00
committed by GitHub
parent 805397ea18
commit 8bf2f4f4d0
7 changed files with 77 additions and 191 deletions

View File

@ -26,8 +26,8 @@ Schema:
"current_height": integer,
"maximum_width": integer,
"maximum_height": integer,
"associated_device": {
"associated_modes": [
"devices": {
"modes": [
{
"resolution_width": integer,
"resolution_height": integer,
@ -58,24 +58,6 @@ Schema:
"reflection": string
}
],
"unassociated_devices": [
{
"associated_modes": [
{
"resolution_width": integer,
"resolution_height": integer,
"is_high_resolution": boolean,
"frequencies": [
{
"frequency": float,
"is_current": boolean,
"is_preferred": boolean
}
]
}
]
}
]
}
Examples:
@ -91,8 +73,8 @@ Examples:
"current_height": 1080,
"maximum_width": 32767,
"maximum_height": 32767,
"associated_device": {
"associated_modes": [
"devices": {
"modes": [
{
"resolution_width": 1920,
"resolution_height": 1080,
@ -136,8 +118,7 @@ Examples:
"reflection": "normal"
}
}
],
"unassociated_devices": []
]
}
$ xrandr --properties | jc --xrandr -p
@ -151,8 +132,8 @@ Examples:
"current_height": 1080,
"maximum_width": 32767,
"maximum_height": 32767,
"associated_device": {
"associated_modes": [
"devices": {
"modes": [
{
"resolution_width": 1920,
"resolution_height": 1080,
@ -199,8 +180,7 @@ Examples:
"reflection": "normal"
}
}
],
"unassociated_devices": []
]
}
"""
import re
@ -212,14 +192,15 @@ from jc.parsers.pyedid.helpers.edid_helper import EdidHelper
class info:
"""Provides parser metadata (version, author, etc.)"""
version = "1.2"
description = "`xrandr` command parser"
author = "Kevin Lyter"
author_email = "lyter_git at sent.com"
details = 'Using parts of the pyedid library at https://github.com/jojonas/pyedid.'
author_email = "code (at) lyterk.com"
details = "Using parts of the pyedid library at https://github.com/jojonas/pyedid."
compatible = ["linux", "darwin", "cygwin", "aix", "freebsd"]
magic_commands = ["xrandr"]
tags = ['command']
tags = ["command"]
__version__ = info.version
@ -267,7 +248,7 @@ try:
"offset_height": int,
"dimension_width": int,
"dimension_height": int,
"associated_modes": List[Mode],
"modes": List[Mode],
"rotation": str,
"reflection": str,
},
@ -282,14 +263,13 @@ try:
"current_height": int,
"maximum_width": int,
"maximum_height": int,
"associated_device": Device,
"devices": List[Device],
},
)
Response = TypedDict(
"Response",
{
"screens": List[Screen],
"unassociated_devices": List[Device],
},
)
except ImportError:
@ -317,14 +297,17 @@ def _parse_screen(next_lines: List[str]) -> Optional[Screen]:
return None
raw_matches = result.groupdict()
screen: Screen = {}
screen: Screen = {"devices": []}
for k, v in raw_matches.items():
screen[k] = int(v)
if next_lines:
while next_lines:
device: Optional[Device] = _parse_device(next_lines)
if device:
screen["associated_device"] = device
if not device:
break
else:
screen["devices"].append(device)
return screen
@ -358,7 +341,7 @@ def _parse_device(next_lines: List[str], quiet: bool = False) -> Optional[Device
matches = result.groupdict()
device: Device = {
"associated_modes": [],
"modes": [],
"is_connected": matches["is_connected"] == "connected",
"is_primary": matches["is_primary"] is not None
and len(matches["is_primary"]) > 0,
@ -367,11 +350,18 @@ def _parse_device(next_lines: List[str], quiet: bool = False) -> Optional[Device
"reflection": matches["reflection"] or "normal",
}
for k, v in matches.items():
if k not in {"is_connected", "is_primary", "device_name", "rotation", "reflection"}:
if k not in {
"is_connected",
"is_primary",
"device_name",
"rotation",
"reflection",
}:
try:
if v:
device[k] = int(v)
except ValueError and not quiet:
except ValueError:
if not quiet:
jc.utils.warning_message(
[f"{next_line} : {k} - {v} is not int-able"]
)
@ -386,7 +376,7 @@ def _parse_device(next_lines: List[str], quiet: bool = False) -> Optional[Device
next_line = next_lines.pop()
next_mode: Optional[Mode] = _parse_mode(next_line)
if next_mode:
device["associated_modes"].append(next_mode)
device["modes"].append(next_mode)
else:
if re.match(_device_pattern, next_line):
next_lines.append(next_line)
@ -481,7 +471,7 @@ def _parse_mode(line: str) -> Optional[Mode]:
return mode
def parse(data: str, raw: bool =False, quiet: bool =False) -> Dict:
def parse(data: str, raw: bool = False, quiet: bool = False) -> Dict:
"""
Main text parsing function
@ -500,19 +490,12 @@ def parse(data: str, raw: bool =False, quiet: bool =False) -> Dict:
linedata = data.splitlines()
linedata.reverse() # For popping
result: Response = {"screens": [], "unassociated_devices": []}
result: Response = {"screens": []}
if jc.utils.has_data(data):
while linedata:
screen = _parse_screen(linedata)
if screen:
result["screens"].append(screen)
else:
device = _parse_device(linedata, quiet)
if device:
result["unassociated_devices"].append(device)
if not result["unassociated_devices"] and not result["screens"]:
return {}
return result

8
tests/fixtures/generic/xrandr_3.out vendored Normal file
View File

@ -0,0 +1,8 @@
Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 16384 x 16384
test-3-1 disconnected primary (normal left inverted right x axis y axis)
test-3-2 connected 1920x1080+0+0 (normal left inverted right x axis y axis) 521mm x 293mm
1920x1080 60.00*+ 59.94 60.00
1680x1050 60.00 59.88
1400x1050 60.00
1600x900 60.00
1280x1024 75.02 60.02 60.00

File diff suppressed because one or more lines are too long

View File

@ -1,44 +0,0 @@
Screen 0: minimum 320 x 200, current 2806 x 900, maximum 8192 x 8192
LVDS-1 connected primary 1366x768+0+0 (normal left inverted right x axis y axis) 344mm x 194mm
1366x768 60.00*+
1280x720 60.00 59.99 59.86 59.74
1024x768 60.04 60.00
960x720 60.00
928x696 60.05
896x672 60.01
1024x576 59.95 59.96 59.90 59.82
960x600 59.93 60.00
960x540 59.96 59.99 59.63 59.82
800x600 60.00 60.32 56.25
840x525 60.01 59.88
864x486 59.92 59.57
700x525 59.98
800x450 59.95 59.82
640x512 60.02
700x450 59.96 59.88
640x480 60.00 59.94
720x405 59.51 58.99
684x384 59.88 59.85
640x400 59.88 59.98
640x360 59.86 59.83 59.84 59.32
512x384 60.00
512x288 60.00 59.92
480x270 59.63 59.82
400x300 60.32 56.34
432x243 59.92 59.57
320x240 60.05
360x202 59.51 59.13
320x180 59.84 59.32
VGA-1 connected 1440x900+1366+0 normal Y axis (normal left inverted right x axis y axis) 408mm x 255mm
1440x900 59.89*+ 74.98
1280x1024 75.02 60.02
1280x960 60.00
1280x800 74.93 59.81
1152x864 75.00
1024x768 75.03 70.07 60.00
832x624 74.55
800x600 72.19 75.00 60.32 56.25
640x480 75.00 72.81 66.67 59.94
720x400 70.08
HDMI-1 disconnected (normal left inverted right x axis y axis)
DP-1 disconnected (normal left inverted right x axis y axis)

File diff suppressed because one or more lines are too long

View File

@ -1,44 +0,0 @@
Screen 0: minimum 320 x 200, current 1846 x 768, maximum 8192 x 8192
LVDS-1 connected primary 1366x768+0+0 (normal left inverted right x axis y axis) 344mm x 194mm
1366x768 60.00*+
1280x720 60.00 59.99 59.86 59.74
1024x768 60.04 60.00
960x720 60.00
928x696 60.05
896x672 60.01
1024x576 59.95 59.96 59.90 59.82
960x600 59.93 60.00
960x540 59.96 59.99 59.63 59.82
800x600 60.00 60.32 56.25
840x525 60.01 59.88
864x486 59.92 59.57
700x525 59.98
800x450 59.95 59.82
640x512 60.02
700x450 59.96 59.88
640x480 60.00 59.94
720x405 59.51 58.99
684x384 59.88 59.85
640x400 59.88 59.98
640x360 59.86 59.83 59.84 59.32
512x384 60.00
512x288 60.00 59.92
480x270 59.63 59.82
400x300 60.32 56.34
432x243 59.92 59.57
320x240 60.05
360x202 59.51 59.13
320x180 59.84 59.32
VGA-1 connected 480x640+1366+0 left (normal left inverted right x axis y axis) 408mm x 255mm
1440x900 59.89 + 74.98
1280x1024 75.02 60.02
1280x960 60.00
1280x800 74.93 59.81
1152x864 75.00
1024x768 75.03 70.07 60.00
832x624 74.55
800x600 72.19 75.00 60.32 56.25
640x480 75.00* 72.81 66.67 59.94
720x400 70.08
HDMI-1 disconnected (normal left inverted right x axis y axis)
DP-1 disconnected (normal left inverted right x axis y axis)

View File

@ -21,13 +21,15 @@ from jc.parsers.xrandr import (
Screen,
)
import pprint
class XrandrTests(unittest.TestCase):
def test_xrandr_nodata(self):
"""
Test 'xrandr' with no data
"""
self.assertEqual(parse('', quiet=True), {})
self.assertEqual(parse("", quiet=True), {"screens": []})
def test_regexes(self):
devices = [
@ -44,7 +46,7 @@ class XrandrTests(unittest.TestCase):
screens = [
"Screen 0: minimum 8 x 8, current 1920 x 1080, maximum 32767 x 32767",
"Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 16384 x 16384"
"Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 16384 x 16384",
]
for screen in screens:
self.assertIsNotNone(re.match(_screen_pattern, screen))
@ -73,7 +75,7 @@ class XrandrTests(unittest.TestCase):
" 360098ff1000001c000000fd00374b1e ",
" 530f000a202020202020000000fc0041 ",
" 535553205657313933530a20000000ff ",
" 0037384c383032313130370a20200077 "
" 0037384c383032313130370a20200077 ",
]
for i in range(len(edid_lines)):
@ -104,7 +106,9 @@ class XrandrTests(unittest.TestCase):
for k, v in expected.items():
self.assertEqual(v, actual[k], f"screens regex failed on {k}")
sample = "Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 16384 x 16384"
sample = (
"Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 16384 x 16384"
)
actual = _parse_screen([sample])
if actual:
self.assertEqual(320, actual["minimum_width"])
@ -141,9 +145,7 @@ class XrandrTests(unittest.TestCase):
device = _parse_device(extended_sample)
if device:
self.assertEqual(
59.94, device["associated_modes"][12]["frequencies"][4]["frequency"]
)
self.assertEqual(59.94, device["modes"][12]["frequencies"][4]["frequency"])
def test_device_with_reflect(self):
sample = "VGA-1 connected primary 1920x1080+0+0 left X and Y axis (normal left inverted right x axis y axis) 310mm x 170mm"
@ -195,67 +197,49 @@ class XrandrTests(unittest.TestCase):
self.assertEqual(True, actual["is_high_resolution"])
self.assertEqual(50.0, actual["frequencies"][1]["frequency"])
def test_complete(self):
def test_complete_1(self):
self.maxDiff = None
with open("tests/fixtures/generic/xrandr.out", "r") as f:
txt = f.read()
actual = parse(txt, quiet=True)
self.assertEqual(1, len(actual["screens"]))
self.assertEqual(4, len(actual["unassociated_devices"]))
self.assertEqual(
18, len(actual["screens"][0]["associated_device"]["associated_modes"])
)
self.assertEqual(18, len(actual["screens"][0]["devices"][0]["modes"]))
def test_complete_2(self):
with open("tests/fixtures/generic/xrandr_2.out", "r") as f:
txt = f.read()
actual = parse(txt, quiet=True)
self.assertEqual(1, len(actual["screens"]))
self.assertEqual(3, len(actual["unassociated_devices"]))
self.assertEqual(38, len(actual["screens"][0]["devices"][0]["modes"]))
def test_complete_3(self):
with open("tests/fixtures/generic/xrandr_3.out", "r") as f:
txt = f.read()
actual = parse(txt, quiet=True)
self.assertEqual(1, len(actual["screens"]))
self.assertEqual(
38, len(actual["screens"][0]["associated_device"]["associated_modes"])
2,
len(actual["screens"][0]["devices"]),
)
def test_complete_4(self):
with open("tests/fixtures/generic/xrandr_simple.out", "r") as f:
txt = f.read()
actual = parse(txt, quiet=True)
self.assertEqual(1, len(actual["screens"]))
self.assertEqual(0, len(actual["unassociated_devices"]))
self.assertEqual(
2, len(actual["screens"][0]["associated_device"]["associated_modes"])
)
self.assertEqual(2, len(actual["screens"][0]["devices"][0]["modes"]))
def test_complete_5(self):
with open("tests/fixtures/generic/xrandr_properties.out", "r") as f:
txt = f.read()
actual = parse(txt, quiet=True)
self.assertEqual(1, len(actual["screens"]))
self.assertEqual(3, len(actual["unassociated_devices"]))
self.assertEqual(
29, len(actual["screens"][0]["associated_device"]["associated_modes"])
)
def test_infinite_loop_fix(self):
with open("tests/fixtures/generic/xrandr_fix_spaces.out", "r") as f:
txt = f.read()
actual = parse(txt, quiet=True)
with open("tests/fixtures/generic/xrandr_fix_spaces.json", "r") as f:
json_dict = json.loads(f.read())
self.assertEqual(actual, json_dict)
def test_is_current_fix(self):
with open("tests/fixtures/generic/xrandr_is_current_fix.out", "r") as f:
txt = f.read()
actual = parse(txt, quiet=True)
with open("tests/fixtures/generic/xrandr_is_current_fix.json", "r") as f:
json_dict = json.loads(f.read())
self.assertEqual(actual, json_dict)
self.assertEqual(29, len(actual["screens"][0]["devices"][0]["modes"]))
def test_model(self):
asus_edid = [
@ -267,7 +251,7 @@ class XrandrTests(unittest.TestCase):
" 360098ff1000001c000000fd00374b1e",
" 530f000a202020202020000000fc0041",
" 535553205657313933530a20000000ff",
" 0037384c383032313130370a20200077"
" 0037384c383032313130370a20200077",
]
asus_edid.reverse()
@ -293,7 +277,7 @@ class XrandrTests(unittest.TestCase):
" 250058c2100000190000000f00000000",
" 000000000025d9066a00000000fe0053",
" 414d53554e470a204ca34154000000fe",
" 004c544e313536415432343430310018"
" 004c544e313536415432343430310018",
]
generic_edid.reverse()
@ -314,5 +298,6 @@ class XrandrTests(unittest.TestCase):
actual: Optional[Model] = _parse_model(empty_edid)
self.assertIsNone(actual)
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()