Merge ~pieq/plainbox-provider-checkbox:1968943-rework-max-resolution-job into plainbox-provider-checkbox:master

Proposed by Pierre Equoy
Status: Merged
Approved by: Sylvain Pineau
Approved revision: a028a44bb18f0f2e9e26c91c3e7f216c70911509
Merged at revision: 91478e881f1672cb9c9a3f68fd7b2d31b6602b07
Proposed branch: ~pieq/plainbox-provider-checkbox:1968943-rework-max-resolution-job
Merge into: plainbox-provider-checkbox:master
Diff against target: 289 lines (+149/-99)
4 files modified
bin/graphics_max_resolution.py (+148/-0)
dev/null (+0/-74)
units/graphics/jobs.pxu (+1/-17)
units/graphics/legacy.pxu (+0/-8)
Reviewer Review Type Date Requested Status
Devices Certification Bot Needs Fixing
Sylvain Pineau (community) Approve
Review via email: mp+424146@code.launchpad.net

Description of the change

See commit messages for more information.

This change provides more information when running the .*maximum_resolution.* Checkbox job, and is compatible with 20.04 and 22.04, on X11 and Wayland.

These changes have been tested on 20.04 and 22.04 using X11 and Wayland on two different devices:
- a laptop with a UHD screen (3840x2160) and an external FHD monitor (1920x1080) connected to it
- a desktop with a UHD monitor (3840x2160) connected to it

I tested a bunch of different scenarios:

- different resolutions
- different scale factors
- different GNOME settings ("Join Displays", "Mirror", "Single Display")

The results are available here:

https://pastebin.ubuntu.com/p/r5xB5PfZtJ/

Pay attention to the output as well as the choice selected by default in Checkbox (passed/failed) which reflects the script outcome.

Additionally, this MR also remove an unused script and job from the provider, since they are not used in any of our test plans.

To post a comment you must log in.
Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

tested as well on 20.04 and 22.04. all good.

But I doubt flake8 will let the last long line to land!

review: Needs Fixing
Revision history for this message
Pierre Equoy (pieq) wrote :

Argh. I ran black on it, I thought that was enough...

Anyway, I reformatted and made sure it passes flake8.

Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

thx, +1

review: Approve
Revision history for this message
Devices Certification Bot (ce-certification-qa) wrote :

The merge was fine but running tests failed.

"10.38.105.108"
"10.38.105.54"
"10.38.105.197"
[bionic] [09:42:36] starting container
[xenial] [09:42:37] starting container
Device project added to bionic-testing
Device project added to xenial-testing
"10.38.105.54"
[xenial] [09:42:56] provisioning container
"10.38.105.51"
[bionic] [09:42:58] provisioning container
[focal] [09:43:09] starting container
Device project added to focal-testing
"10.38.105.60"
[focal] [09:43:34] provisioning container
[xenial] [09:44:32] Starting tests...
[xenial] Found a test script: ./requirements/container-tests-provider-checkbox
[bionic] [09:44:41] Starting tests...
[bionic] Found a test script: ./requirements/container-tests-provider-checkbox
[xenial] [09:45:54] container-tests-provider-checkbox: FAIL
[xenial] output: https://paste.ubuntu.com/p/wJwZVrvfkV/
[xenial] [09:45:57] Fixing file permissions in source directory
[xenial] [09:45:57] Destroying container
[bionic] [09:46:55] container-tests-provider-checkbox: PASS
[bionic] [09:46:55] Fixing file permissions in source directory
[bionic] [09:46:55] Destroying container
[focal] [09:48:13] Starting tests...
[focal] Found a test script: ./requirements/container-tests-provider-checkbox
[focal] [09:49:41] container-tests-provider-checkbox: PASS
[focal] [09:49:41] Fixing file permissions in source directory
[focal] [09:49:41] Destroying container

review: Needs Fixing
Revision history for this message
Pierre Equoy (pieq) wrote :

Xenial didn't like the f-strings formatting...

Modified this, ran black on 20.04, then flake8 on 20.04 and 18.04, looks good.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/bin/graphics_max_resolution.py b/bin/graphics_max_resolution.py
2new file mode 100755
3index 0000000..56628a4
4--- /dev/null
5+++ b/bin/graphics_max_resolution.py
6@@ -0,0 +1,148 @@
7+#!/usr/bin/env python3
8+#
9+# This file is part of Checkbox.
10+#
11+# Copyright 2022 Canonical Ltd.
12+#
13+# Authors:
14+# Pierre Equoy <pierre.equoy@canonical.com>
15+#
16+# Checkbox is free software: you can redistribute it and/or modify
17+# it under the terms of the GNU General Public License version 3,
18+# as published by the Free Software Foundation.
19+#
20+# Checkbox is distributed in the hope that it will be useful,
21+# but WITHOUT ANY WARRANTY; without even the implied warranty of
22+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23+# GNU General Public License for more details.
24+#
25+# You should have received a copy of the GNU General Public License
26+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
27+
28+import gi
29+from glob import glob
30+import os
31+import sys
32+from pathlib import Path
33+
34+gi.require_versions({"Gtk": "3.0", "Gdk": "3.0"})
35+from gi.repository import Gdk, Gtk # noqa: E402
36+
37+
38+def get_sysfs_info():
39+ """
40+ Go through each graphics cards sysfs entries to find max resolution if
41+ connected to a monitor.
42+ Return a list of ports with information about them.
43+ """
44+ ports = glob("/sys/class/drm/card*-*")
45+ entries = []
46+ for p in ports:
47+ with open(Path(p) / "modes") as f:
48+ # Topmost line in the modes file is the max resolution
49+ max_resolution = f.readline().strip()
50+ if max_resolution:
51+ # e.g. "/sys/class/drm/card0-HDMI-A-1"
52+ port = p.split("/")[-1]
53+ width, height = max_resolution.split("x")
54+ with open(Path(p) / "enabled") as f:
55+ enabled = f.readline().strip()
56+ with open(Path(p) / "dpms") as f:
57+ dpms = f.readline().strip()
58+ with open(Path(p) / "status") as f:
59+ status = f.readline().strip()
60+ port_info = {
61+ "port": port,
62+ "width": int(width),
63+ "height": int(height),
64+ "enabled": enabled, # "enabled" or "disabled"
65+ "status": status, # "connected" or "disconnected"
66+ "dpms": dpms, # "On" or "Off"
67+ }
68+ entries.append(port_info)
69+ return entries
70+
71+
72+def get_monitors_info():
73+ """
74+ Get information (model, manufacturer, resolution) from each connected
75+ monitors using Gtk.
76+ Return a list of monitors with their information.
77+ """
78+ Gtk.init()
79+ display = Gdk.Display.get_default()
80+ monitors = []
81+ for i in range(display.get_n_monitors()):
82+ mon = display.get_monitor(i)
83+ monitor = {
84+ "model": mon.get_model(),
85+ "manufacturer": mon.get_manufacturer(),
86+ "width": mon.get_geometry().width,
87+ "height": mon.get_geometry().height,
88+ "scale_factor": mon.get_scale_factor(),
89+ }
90+ monitors.append(monitor)
91+ return monitors
92+
93+
94+if __name__ == "__main__":
95+ sysfs_entries = get_sysfs_info()
96+ mons_entries = get_monitors_info()
97+ total_sysfs_res = 0
98+ total_mons_res = 0
99+ compositor = os.environ.get("XDG_SESSION_TYPE")
100+ print("Current compositor: {}".format(compositor))
101+ print()
102+ print("Maximum resolution found for each connected monitors:")
103+ for p in sysfs_entries:
104+ port = p["port"]
105+ width = p["width"]
106+ height = p["height"]
107+ enabled = p["enabled"]
108+ status = p["status"]
109+ dpms = p["dpms"].lower()
110+ print(
111+ "\t{}: {}x{} ({}, {}, {})".format(
112+ port, width, height, dpms, status, enabled
113+ )
114+ )
115+ # If the monitor is disabled (e.g. "Single Display" mode), don't take
116+ # its surface into account.
117+ if enabled == "enabled":
118+ total_sysfs_res += width * height
119+ print()
120+ print("Current resolution found for each connected monitors:")
121+ for m in mons_entries:
122+ model = m["model"]
123+ manufacturer = m["manufacturer"]
124+ scale = m["scale_factor"]
125+ # Under X11, the returned width and height are in "application pixels",
126+ # not "device pixels", so it has to be multiplied by the scale factor.
127+ # However, Wayland always returns the "device pixels" width and height.
128+ #
129+ # Example: a 3840x2160 screen set to 200% scale will have
130+ # width = 1920, height = 1080, scale_factor = 2 on X11
131+ # width = 3840, height = 2160, scale_factor = 2 on Wayland
132+ if compositor == "x11":
133+ width = m["width"] * m["scale_factor"]
134+ height = m["height"] * m["scale_factor"]
135+ else:
136+ width = m["width"]
137+ height = m["height"]
138+ print(
139+ "\t{} ({}): {}x{} @{}%".format(
140+ model, manufacturer, width, height, scale * 100
141+ )
142+ )
143+ total_mons_res += width * height
144+ print()
145+ if total_sysfs_res == total_mons_res:
146+ print("The displays are configured at their maximum resolution.")
147+ else:
148+ sys.exit(
149+ (
150+ "The displays do not seem to be configured at their maximum "
151+ "resolution.\nPlease switch to the maximum resolution before "
152+ "continuing."
153+ )
154+ )
155diff --git a/bin/graphics_modes_info.py b/bin/graphics_modes_info.py
156deleted file mode 100755
157index f92cd76..0000000
158--- a/bin/graphics_modes_info.py
159+++ /dev/null
160@@ -1,74 +0,0 @@
161-#!/usr/bin/env python3
162-# -*- coding: utf-8 -*-
163-#
164-# graphics_modes_info.py
165-#
166-# This file is part of Checkbox.
167-#
168-# Copyright 2012 Canonical Ltd.
169-#
170-# Authors: Alberto Milone <alberto.milone@canonical.com>
171-#
172-# Checkbox is free software: you can redistribute it and/or modify
173-# it under the terms of the GNU General Public License version 3,
174-# as published by the Free Software Foundation.
175-
176-#
177-# Checkbox is distributed in the hope that it will be useful,
178-# but WITHOUT ANY WARRANTY; without even the implied warranty of
179-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
180-# GNU General Public License for more details.
181-#
182-# You should have received a copy of the GNU General Public License
183-# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
184-
185-from __future__ import print_function
186-from __future__ import unicode_literals
187-import sys
188-
189-from checkbox_support.contrib import xrandr
190-
191-
192-def print_modes_info(screen):
193- """Print some information about the detected screen and its outputs"""
194- xrandr._check_required_version((1, 0))
195- print("Screen %s: minimum %s x %s, current %s x %s, maximum %s x %s" %
196- (screen._screen,
197- screen._width_min, screen._height_min,
198- screen._width, screen._height,
199- screen._width_max, screen._height_max))
200- print(" %smm x %smm" % (screen._width_mm, screen._height_mm))
201- print("Outputs:")
202- for o in list(screen.outputs.keys()):
203- output = screen.outputs[o]
204- print(" %s" % o, end=' ')
205- if output.is_connected():
206- print("(%smm x %smm)" % (output.get_physical_width(),
207- output.get_physical_height()))
208- modes = output.get_available_modes()
209- print(" Modes:")
210- for m in range(len(modes)):
211- mode = modes[m]
212- refresh = mode.dotClock / (mode.hTotal * mode.vTotal)
213- print(
214- " [%s] %s x %s @ %s Hz" %
215- (m, mode.width, mode.height, refresh), end=' ')
216- if mode.id == output._mode:
217- print("(current)", end=' ')
218- if m == output.get_preferred_mode():
219- print("(preferred)", end=' ')
220- print("")
221- else:
222- print("(not connected)")
223-
224-
225-def main():
226- screen = xrandr.get_current_screen()
227- try:
228- print_modes_info(screen)
229- except(xrandr.UnsupportedRRError):
230- print('Error: RandR version lower than 1.0', file=sys.stderr)
231-
232-
233-if __name__ == '__main__':
234- main()
235diff --git a/units/graphics/jobs.pxu b/units/graphics/jobs.pxu
236index 4dde304..8206f1a 100644
237--- a/units/graphics/jobs.pxu
238+++ b/units/graphics/jobs.pxu
239@@ -133,13 +133,7 @@ category_id: com.canonical.plainbox::graphics
240 requires:
241 device.category == 'VIDEO'
242 command:
243- # shellcheck disable=SC1091
244- source graphics_env.sh {driver} {index}
245- maxi="$(xrandr -q |grep -A 1 "connected\( primary\)* [0-9]" |tail -1 |awk '{{print $1}}')"
246- now="$(python3 -c 'from gi.repository import Gdk; screen=Gdk.Screen.get_default(); geo = screen.get_monitor_geometry(screen.get_primary_monitor()); print(geo.width, "x", geo.height, sep="")')"
247- test "$maxi" != "$now" && notify="\nPlease switch to the maximum resolution \nfor every graphic tests"
248- echo "Maximum resolution: $maxi"
249- echo "Current resolution: $now $notify"
250+ graphics_max_resolution.py
251 estimated_duration: 10.0
252 _summary: Test maximum supported resolution for {vendor} {product}
253 _description:
254@@ -154,16 +148,6 @@ _description:
255
256 unit: template
257 template-resource: graphics_card
258-id: graphics/{index}_modes_{product_slug}
259-plugin: shell
260-category_id: com.canonical.plainbox::graphics
261-command: graphics_modes_info.py
262-estimated_duration: 0.250
263-_description: Collect info on graphics modes (screen resolution and refresh rate) for {vendor} {product}
264-_summary: Test graphic modes info for {vendor} {product}
265-
266-unit: template
267-template-resource: graphics_card
268 id: graphics/{index}_color_depth_{product_slug}
269 plugin: shell
270 category_id: com.canonical.plainbox::graphics
271diff --git a/units/graphics/legacy.pxu b/units/graphics/legacy.pxu
272index 14969b0..2b9545d 100644
273--- a/units/graphics/legacy.pxu
274+++ b/units/graphics/legacy.pxu
275@@ -96,14 +96,6 @@ _description:
276 VERIFICATION:
277 Is this the display's maximum resolution?
278
279-id: graphics/modes
280-plugin: shell
281-category_id: com.canonical.plainbox::graphics
282-command: graphics_modes_info.py
283-estimated_duration: 0.250
284-_description: Collect info on graphics modes (screen resolution and refresh rate)
285-_summary: Collect info on graphics modes
286-
287 id: graphics/color_depth
288 plugin: shell
289 category_id: com.canonical.plainbox::graphics

Subscribers

People subscribed via source and target branches