summaryrefslogtreecommitdiff
path: root/bin
diff options
Diffstat (limited to 'bin')
-rwxr-xr-xbin/accelerometer_test6
-rwxr-xr-xbin/audio_test1
-rwxr-xr-xbin/battery_test2
-rwxr-xr-xbin/boot_mode_test_snappy.py125
-rwxr-xr-xbin/camera_test_legacy578
-rwxr-xr-xbin/check-prerelease137
-rwxr-xr-xbin/cycle_vts6
-rwxr-xr-xbin/disk_info2
-rwxr-xr-xbin/disk_read_performance_test2
-rwxr-xr-xbin/efi-pxeboot3
-rwxr-xr-xbin/fde_tests.py101
-rwxr-xr-xbin/gatt-notify-test.py241
-rwxr-xr-xbin/gpu_test2
-rwxr-xr-xbin/graphics_driver5
-rwxr-xr-xbin/graphics_env2
-rwxr-xr-xbin/gst_pipeline_test3
-rwxr-xr-xbin/ipmi_test8
-rwxr-xr-xbin/key_test4
-rwxr-xr-xbin/keyboard_test4
-rwxr-xr-xbin/lock_screen_watcher2
-rwxr-xr-xbin/manage_compiz_plugin9
-rwxr-xr-xbin/net_if_watcher.py43
-rwxr-xr-xbin/network_restart3
-rwxr-xr-xbin/pm_test93
-rwxr-xr-xbin/removable_storage_test18
-rwxr-xr-xbin/resolution_test3
-rwxr-xr-xbin/socketcan_test.py199
-rwxr-xr-xbin/touchpad_test4
-rwxr-xr-xbin/xrandr_cycle7
29 files changed, 980 insertions, 633 deletions
diff --git a/bin/accelerometer_test b/bin/accelerometer_test
index 93130fc..2d95ca8 100755
--- a/bin/accelerometer_test
+++ b/bin/accelerometer_test
@@ -24,13 +24,17 @@ accelerometer, and check to be sure that the x, y, z axis respond
to physical movement of hardware.
'''
from argparse import ArgumentParser
-from gi.repository import Gdk, GLib, Gtk
+import gi
import logging
import os
import re
import sys
import threading
import time
+gi.require_version('Gdk', '3.0')
+gi.require_version('GLib', '2.0')
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gdk, GLib, Gtk
from subprocess import Popen, PIPE, check_output, STDOUT, CalledProcessError
from checkbox_support.parsers.modinfo import ModinfoParser
diff --git a/bin/audio_test b/bin/audio_test
index f0b73a4..466b760 100755
--- a/bin/audio_test
+++ b/bin/audio_test
@@ -12,6 +12,7 @@ import sys
import time
try:
import gi
+ gi.require_version('GLib', '2.0')
gi.require_version('Gst','1.0')
from gi.repository import GObject
from gi.repository import Gst
diff --git a/bin/battery_test b/bin/battery_test
index cdfd227..b2682c7 100755
--- a/bin/battery_test
+++ b/bin/battery_test
@@ -1,11 +1,13 @@
#!/usr/bin/env python3
+import gi
import os
import time
import re
import subprocess
import sys
import argparse
+gi.require_version('Gio', '2.0')
from gi.repository import Gio
diff --git a/bin/boot_mode_test_snappy.py b/bin/boot_mode_test_snappy.py
new file mode 100755
index 0000000..46bed92
--- /dev/null
+++ b/bin/boot_mode_test_snappy.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python3
+# Copyright 2018 Canonical Ltd.
+# Written by:
+# Jonathan Cave <jonathan.cave@canonical.com>
+
+import io
+import os
+import re
+import sys
+import subprocess as sp
+
+import yaml
+
+
+def fitdumpimage(filename):
+ cmd = 'dumpimage -l {}'.format(filename)
+ out = sp.check_output(cmd, shell=True).decode(sys.stdout.encoding)
+ buf = io.StringIO(out)
+
+ # first line should identify FIT file
+ if not buf.readline().startswith('FIT description'):
+ raise SystemExit('ERROR: expected FIT image description')
+
+ # second line contains some metadata, skip it
+ buf.readline()
+
+ # from then on should get blocks of text describing the objects that were
+ # combined in to the FIT image e.g. kernel, ramdisk, device tree
+ image_re = re.compile(r'(?:^\ Image)\ \d+\ \((\S+)\)$')
+ config_re = re.compile(r'^\ Default Configuration|^\ Configuration')
+ objects = {}
+ name = ''
+ while True:
+ line = buf.readline()
+ # stop at end
+ if line == '':
+ break
+ # interested in storing image information
+ match = image_re.search(line)
+ if match:
+ name = match.group(1)
+ objects[name] = {}
+ continue
+ # not interested in configurations
+ if config_re.search(line):
+ name = ''
+ continue
+ # while in an image section store the info
+ if name != '':
+ entries = [s.strip() for s in line.split(':', 1)]
+ objects[name][entries[0]] = entries[1]
+ return objects
+
+
+def main():
+ if len(sys.argv) != 3:
+ raise SystemExit('ERROR: please supply gadget & kernel name')
+ gadget = sys.argv[1]
+ kernel = sys.argv[2]
+
+ gadget_yaml = os.path.join('/snap', gadget, 'current/meta/gadget.yaml')
+
+ if not os.path.exists(gadget_yaml):
+ raise SystemExit(
+ 'ERROR: failed to find gadget.yaml at {}'.format(gadget_yaml))
+
+ with open(gadget_yaml) as f:
+ data = yaml.load(f)
+ for k in data['volumes'].keys():
+ bootloader = data['volumes'][k]['bootloader']
+ if not bootloader:
+ raise SystemExit('ERROR: could not find name of bootloader')
+
+ if bootloader not in ('u-boot', 'grub'):
+ raise SystemExit(
+ 'ERROR: Unexpected bootloader name {}'.format(bootloader))
+ print('Bootloader is {}\n'.format(bootloader))
+
+ if bootloader == 'u-boot':
+ print('Parsing FIT image information...\n')
+
+ kernel_rev = os.path.basename(
+ os.path.realpath('/snap/{}/current'.format(kernel)))
+ boot_kernel = '/boot/uboot/{}_{}.snap/kernel.img'.format(
+ kernel, kernel_rev)
+ boot_objects = fitdumpimage(boot_kernel)
+
+ for obj, attrs in boot_objects.items():
+ print('Checking object {}'.format(obj))
+ if 'Sign value' not in attrs:
+ raise SystemExit('ERROR: no sign value found for object')
+ print('Found "Sign value"')
+ if len(attrs['Sign value']) != 512:
+ raise SystemExit('ERROR: unexpected sign value size')
+ if all(s in attrs['Sign algo'] for s in ['sha256', 'rsa2048']):
+ print('Found expected signing algorithms')
+ else:
+ raise SystemExit(
+ 'ERROR: unexpected signing algorithms {}'.format(
+ attrs['Sign algo']))
+ print()
+
+ # check that all parts of the fit image have
+ snap_kernel = '/snap/{}/current/kernel.img'.format(kernel)
+ snap_objects = fitdumpimage(snap_kernel)
+ if snap_objects != boot_objects:
+ raise SystemExit(
+ 'ERROR: boot kernel and current snap kernel do not match')
+ print('Kernel images in current snap and u-boot match\n')
+
+ print('Secure Boot appears to be enabled on this system')
+
+ if bootloader == 'grub':
+ cmd = 'mokutil --sb-state'
+ print('+', cmd, flush=True)
+ out = sp.check_output(cmd, shell=True).decode(sys.stdout.encoding)
+ print(out, flush=True)
+ if out != 'SecureBoot enabled\n':
+ raise SystemExit('ERROR: mokutil reports Secure Boot not in use')
+
+ print('Secure Boot appears to be enabled on this system')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/camera_test_legacy b/bin/camera_test_legacy
deleted file mode 100755
index 794578b..0000000
--- a/bin/camera_test_legacy
+++ /dev/null
@@ -1,578 +0,0 @@
-#!/usr/bin/env python3
-#
-# This file is part of Checkbox.
-#
-# Copyright 2008-2012 Canonical Ltd.
-#
-# The v4l2 ioctl code comes from the Python bindings for the v4l2
-# userspace api (http://pypi.python.org/pypi/v4l2):
-# Copyright (C) 1999-2009 the contributors
-#
-# The JPEG metadata parser is a part of bfg-pages:
-# http://code.google.com/p/bfg-pages/source/browse/trunk/pages/getimageinfo.py
-# Copyright (C) Tim Hoffman
-#
-# Checkbox is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 3,
-# as published by the Free Software Foundation.
-
-#
-# Checkbox is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
-#
-
-import argparse
-import ctypes
-import errno
-import fcntl
-import imghdr
-import logging
-import os
-import re
-import struct
-import sys
-import time
-
-from gi.repository import GObject
-from glob import glob
-from subprocess import check_call, CalledProcessError, STDOUT
-from tempfile import NamedTemporaryFile
-
-
-_IOC_NRBITS = 8
-_IOC_TYPEBITS = 8
-_IOC_SIZEBITS = 14
-
-_IOC_NRSHIFT = 0
-_IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS
-_IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS
-_IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS
-
-_IOC_WRITE = 1
-_IOC_READ = 2
-
-
-def _IOC(dir_, type_, nr, size):
- return (
- ctypes.c_int32(dir_ << _IOC_DIRSHIFT).value |
- ctypes.c_int32(ord(type_) << _IOC_TYPESHIFT).value |
- ctypes.c_int32(nr << _IOC_NRSHIFT).value |
- ctypes.c_int32(size << _IOC_SIZESHIFT).value)
-
-
-def _IOC_TYPECHECK(t):
- return ctypes.sizeof(t)
-
-
-def _IOR(type_, nr, size):
- return _IOC(_IOC_READ, type_, nr, ctypes.sizeof(size))
-
-
-def _IOWR(type_, nr, size):
- return _IOC(_IOC_READ | _IOC_WRITE, type_, nr, _IOC_TYPECHECK(size))
-
-
-class v4l2_capability(ctypes.Structure):
- """
- Driver capabilities
- """
- _fields_ = [
- ('driver', ctypes.c_char * 16),
- ('card', ctypes.c_char * 32),
- ('bus_info', ctypes.c_char * 32),
- ('version', ctypes.c_uint32),
- ('capabilities', ctypes.c_uint32),
- ('reserved', ctypes.c_uint32 * 4),
- ]
-
-
-# Values for 'capabilities' field
-V4L2_CAP_VIDEO_CAPTURE = 0x00000001
-V4L2_CAP_VIDEO_OVERLAY = 0x00000004
-V4L2_CAP_READWRITE = 0x01000000
-V4L2_CAP_STREAMING = 0x04000000
-
-v4l2_frmsizetypes = ctypes.c_uint
-(
- V4L2_FRMSIZE_TYPE_DISCRETE,
- V4L2_FRMSIZE_TYPE_CONTINUOUS,
- V4L2_FRMSIZE_TYPE_STEPWISE,
-) = range(1, 4)
-
-
-class v4l2_frmsize_discrete(ctypes.Structure):
- _fields_ = [
- ('width', ctypes.c_uint32),
- ('height', ctypes.c_uint32),
- ]
-
-
-class v4l2_frmsize_stepwise(ctypes.Structure):
- _fields_ = [
- ('min_width', ctypes.c_uint32),
- ('min_height', ctypes.c_uint32),
- ('step_width', ctypes.c_uint32),
- ('min_height', ctypes.c_uint32),
- ('max_height', ctypes.c_uint32),
- ('step_height', ctypes.c_uint32),
- ]
-
-
-class v4l2_frmsizeenum(ctypes.Structure):
- class _u(ctypes.Union):
- _fields_ = [
- ('discrete', v4l2_frmsize_discrete),
- ('stepwise', v4l2_frmsize_stepwise),
- ]
-
- _fields_ = [
- ('index', ctypes.c_uint32),
- ('pixel_format', ctypes.c_uint32),
- ('type', ctypes.c_uint32),
- ('_u', _u),
- ('reserved', ctypes.c_uint32 * 2)
- ]
-
- _anonymous_ = ('_u',)
-
-
-class v4l2_fmtdesc(ctypes.Structure):
- _fields_ = [
- ('index', ctypes.c_uint32),
- ('type', ctypes.c_int),
- ('flags', ctypes.c_uint32),
- ('description', ctypes.c_char * 32),
- ('pixelformat', ctypes.c_uint32),
- ('reserved', ctypes.c_uint32 * 4),
- ]
-
-V4L2_FMT_FLAG_COMPRESSED = 0x0001
-V4L2_FMT_FLAG_EMULATED = 0x0002
-
-# ioctl code for video devices
-VIDIOC_QUERYCAP = _IOR('V', 0, v4l2_capability)
-VIDIOC_ENUM_FRAMESIZES = _IOWR('V', 74, v4l2_frmsizeenum)
-VIDIOC_ENUM_FMT = _IOWR('V', 2, v4l2_fmtdesc)
-
-
-class CameraTest:
- """
- A simple class that displays a test image via GStreamer.
- """
- def __init__(self, args, gst_plugin=None, gst_video_type=None):
- self.args = args
- self._mainloop = GObject.MainLoop()
- self._width = 640
- self._height = 480
- self._gst_plugin = gst_plugin
- self._gst_video_type = gst_video_type
-
- def detect(self):
- """
- Display information regarding webcam hardware
- """
- cap_status = dev_status = 1
- for i in range(10):
- cp = v4l2_capability()
- device = '/dev/video%d' % i
- try:
- with open(device, 'r') as vd:
- fcntl.ioctl(vd, VIDIOC_QUERYCAP, cp)
- except IOError:
- continue
- dev_status = 0
- print("%s: OK" % device)
- print(" name : %s" % cp.card.decode('UTF-8'))
- print(" driver : %s" % cp.driver.decode('UTF-8'))
- print(" version: %s.%s.%s"
- % (cp.version >> 16,
- (cp.version >> 8) & 0xff,
- cp.version & 0xff))
- print(" flags : 0x%x [" % cp.capabilities,
- ' CAPTURE' if cp.capabilities & V4L2_CAP_VIDEO_CAPTURE
- else '',
- ' OVERLAY' if cp.capabilities & V4L2_CAP_VIDEO_OVERLAY
- else '',
- ' READWRITE' if cp.capabilities & V4L2_CAP_READWRITE
- else '',
- ' STREAMING' if cp.capabilities & V4L2_CAP_STREAMING
- else '',
- ' ]', sep="")
-
- resolutions = self._get_supported_resolutions(device)
- print(' ',
- self._supported_resolutions_to_string(resolutions).replace(
- "\n", " "),
- sep="")
-
- if cp.capabilities & V4L2_CAP_VIDEO_CAPTURE:
- cap_status = 0
- return dev_status | cap_status
-
- def led(self):
- """
- Activate camera (switch on led), but don't display any output
- """
- pipespec = ("v4l2src device=%(device)s "
- "! %(type)s "
- "! %(plugin)s "
- "! testsink"
- % {'device': self.args.device,
- 'type': self._gst_video_type,
- 'plugin': self._gst_plugin})
- logging.debug("LED test with pipeline %s", pipespec)
- self._pipeline = Gst.parse_launch(pipespec)
- self._pipeline.set_state(Gst.State.PLAYING)
- time.sleep(3)
- self._pipeline.set_state(Gst.State.NULL)
-
- def display(self):
- """
- Displays the preview window
- """
- pipespec = ("v4l2src device=%(device)s "
- "! %(type)s,width=%(width)d,height=%(height)d "
- "! %(plugin)s "
- "! autovideosink"
- % {'device': self.args.device,
- 'type': self._gst_video_type,
- 'width': self._width,
- 'height': self._height,
- 'plugin': self._gst_plugin})
- logging.debug("display test with pipeline %s", pipespec)
- self._pipeline = Gst.parse_launch(pipespec)
- self._pipeline.set_state(Gst.State.PLAYING)
- time.sleep(10)
- self._pipeline.set_state(Gst.State.NULL)
-
- def still(self):
- """
- Captures an image to a file
- """
- if self.args.filename:
- self._still_helper(self.args.filename, self._width, self._height,
- self.args.quiet)
- else:
- with NamedTemporaryFile(prefix='camera_test_', suffix='.jpg') as f:
- self._still_helper(f.name, self._width, self._height,
- self.args.quiet)
-
- def _still_helper(self, filename, width, height, quiet, pixelformat=None):
- """
- Captures an image to a given filename. width and height specify the
- image size and quiet controls whether the image is displayed to the
- user (quiet = True means do not display image).
- """
- command = ["fswebcam", "-D 1", "-S 50", "--no-banner",
- "-d", self.args.device,
- "-r", "%dx%d"
- % (width, height), filename]
- use_gstreamer = False
- if pixelformat:
- if 'MJPG' == pixelformat: # special tweak for fswebcam
- pixelformat = 'MJPEG'
- command.extend(["-p", pixelformat])
-
- try:
- check_call(command, stdout=open(os.devnull, 'w'), stderr=STDOUT)
- except (CalledProcessError, OSError):
- use_gstreamer = True
-
- if use_gstreamer:
- pipespec = ("v4l2src device=%(device)s "
- "! %(type)s,width=%(width)d,height=%(height)d "
- "! %(plugin)s "
- "! jpegenc "
- "! filesink location=%(filename)s"
- % {'device': self.args.device,
- 'type': self._gst_video_type,
- 'width': width,
- 'height': height,
- 'plugin': self._gst_plugin,
- 'filename': filename})
- logging.debug("still test with gstreamer and "
- "pipeline %s", pipespec)
- self._pipeline = Gst.parse_launch(pipespec)
- self._pipeline.set_state(Gst.State.PLAYING)
- time.sleep(3)
- self._pipeline.set_state(Gst.State.NULL)
-
- if not quiet:
- import imghdr
- image_type = imghdr.what(filename)
- pipespec = ("filesrc location=%(filename)s ! "
- "%(type)sdec ! "
- "videoscale ! "
- "imagefreeze ! autovideosink"
- % {'filename': filename,
- 'type': image_type})
- self._pipeline = Gst.parse_launch(pipespec)
- self._pipeline.set_state(Gst.State.PLAYING)
- time.sleep(10)
- self._pipeline.set_state(Gst.State.NULL)
-
- def _supported_resolutions_to_string(self, supported_resolutions):
- """
- Return a printable string representing a list of supported resolutions
- """
- ret = ""
- for resolution in supported_resolutions:
- ret += "Format: %s (%s)\n" % (resolution['pixelformat'],
- resolution['description'])
- ret += "Resolutions: "
- for res in resolution['resolutions']:
- ret += "%sx%s," % (res[0], res[1])
- # truncate the extra comma with :-1
- ret = ret[:-1] + "\n"
- return ret
-
- def resolutions(self):
- """
- After querying the webcam for supported formats and resolutions,
- take multiple images using the first format returned by the driver,
- and see if they are valid
- """
- resolutions = self._get_supported_resolutions(self.args.device)
- # print supported formats and resolutions for the logs
- print(self._supported_resolutions_to_string(resolutions))
-
- # pick the first format, which seems to be what the driver wants for a
- # default. This also matches the logic that fswebcam uses to select
- # a default format.
- resolution = resolutions[0]
- if resolution:
- print("Taking multiple images using the %s format"
- % resolution['pixelformat'])
- for res in resolution['resolutions']:
- w = res[0]
- h = res[1]
- f = NamedTemporaryFile(prefix='camera_test_%s%sx%s' %
- (resolution['pixelformat'], w, h),
- suffix='.jpg', delete=False)
- print("Taking a picture at %sx%s" % (w, h))
- self._still_helper(f.name, w, h, True,
- pixelformat=resolution['pixelformat'])
- if self._validate_image(f.name, w, h):
- print("Validated image %s" % f.name)
- os.remove(f.name)
- else:
- print("Failed to validate image %s" % f.name,
- file=sys.stderr)
- os.remove(f.name)
- return 1
- return 0
-
- def _get_pixel_formats(self, device, maxformats=5):
- """
- Query the camera to see what pixel formats it supports. A list of
- dicts is returned consisting of format and description. The caller
- should check whether this camera supports VIDEO_CAPTURE before
- calling this function.
- """
- supported_formats = []
- fmt = v4l2_fmtdesc()
- fmt.index = 0
- fmt.type = V4L2_CAP_VIDEO_CAPTURE
- try:
- while fmt.index < maxformats:
- with open(device, 'r') as vd:
- if fcntl.ioctl(vd, VIDIOC_ENUM_FMT, fmt) == 0:
- pixelformat = {}
- # save the int type for re-use later
- pixelformat['pixelformat_int'] = fmt.pixelformat
- pixelformat['pixelformat'] = "%s%s%s%s" % \
- (chr(fmt.pixelformat & 0xFF),
- chr((fmt.pixelformat >> 8) & 0xFF),
- chr((fmt.pixelformat >> 16) & 0xFF),
- chr((fmt.pixelformat >> 24) & 0xFF))
- pixelformat['description'] = fmt.description.decode()
- supported_formats.append(pixelformat)
- fmt.index = fmt.index + 1
- except IOError as e:
- # EINVAL is the ioctl's way of telling us that there are no
- # more formats, so we ignore it
- if e.errno != errno.EINVAL:
- print("Unable to determine Pixel Formats, this may be a "
- "driver issue.")
- return supported_formats
- return supported_formats
-
- def _get_supported_resolutions(self, device):
- """
- Query the camera for supported resolutions for a given pixel_format.
- Data is returned in a list of dictionaries with supported pixel
- formats as the following example shows:
- resolution['pixelformat'] = "YUYV"
- resolution['description'] = "(YUV 4:2:2 (YUYV))"
- resolution['resolutions'] = [[width, height], [640, 480], [1280, 720] ]
-
- If we are unable to gather any information from the driver, then we
- return YUYV and 640x480 which seems to be a safe default.
- Per the v4l2 spec the ioctl used here is experimental
- but seems to be well supported.
- """
- supported_formats = self._get_pixel_formats(device)
- if not supported_formats:
- resolution = {}
- resolution['description'] = "YUYV"
- resolution['pixelformat'] = "YUYV"
- resolution['resolutions'] = [[640, 480]]
- supported_formats.append(resolution)
- return supported_formats
-
- for supported_format in supported_formats:
- resolutions = []
- framesize = v4l2_frmsizeenum()
- framesize.index = 0
- framesize.pixel_format = supported_format['pixelformat_int']
- with open(device, 'r') as vd:
- try:
- while fcntl.ioctl(vd,
- VIDIOC_ENUM_FRAMESIZES,
- framesize) == 0:
- if framesize.type == V4L2_FRMSIZE_TYPE_DISCRETE:
- resolutions.append([framesize.discrete.width,
- framesize.discrete.height])
- # for continuous and stepwise, let's just use min and
- # max they use the same structure and only return
- # one result
- elif (framesize.type in (V4L2_FRMSIZE_TYPE_CONTINUOUS,
- V4L2_FRMSIZE_TYPE_STEPWISE)):
- resolutions.append([framesize.stepwise.min_width,
- framesize.stepwise.min_height]
- )
- resolutions.append([framesize.stepwise.max_width,
- framesize.stepwise.max_height]
- )
- break
- framesize.index = framesize.index + 1
- except IOError as e:
- # EINVAL is the ioctl's way of telling us that there are no
- # more formats, so we ignore it
- if e.errno != errno.EINVAL:
- print("Unable to determine supported framesizes "
- "(resolutions), this may be a driver issue.")
- supported_format['resolutions'] = resolutions
- return supported_formats
-
- def _validate_image(self, filename, width, height):
- """
- Given a filename, ensure that the image is the width and height
- specified and is a valid image file.
- """
- if imghdr.what(filename) != 'jpeg':
- return False
-
- outw = outh = 0
- with open(filename, mode='rb') as jpeg:
- jpeg.seek(2)
- b = jpeg.read(1)
- try:
- while (b and ord(b) != 0xDA):
- while (ord(b) != 0xFF):
- b = jpeg.read(1)
- while (ord(b) == 0xFF):
- b = jpeg.read(1)
- if (ord(b) >= 0xC0 and ord(b) <= 0xC3):
- jpeg.seek(3, 1)
- h, w = struct.unpack(">HH", jpeg.read(4))
- break
- b = jpeg.read(1)
- outw, outh = int(w), int(h)
- except (struct.error, ValueError):
- pass
-
- if outw != width:
- print("Image width does not match, was %s should be %s" %
- (outw, width), file=sys.stderr)
- return False
- if outh != height:
- print("Image width does not match, was %s should be %s" %
- (outh, height), file=sys.stderr)
- return False
-
- return True
-
- return True
-
-
-def parse_arguments(argv):
- """
- Parse command line arguments
- """
- parser = argparse.ArgumentParser(description="Run a camera-related test")
- subparsers = parser.add_subparsers(dest='test',
- title='test',
- description='Available camera tests')
-
- parser.add_argument('--debug', dest='log_level',
- action="store_const", const=logging.DEBUG,
- default=logging.INFO, help="Show debugging messages")
-
- def add_device_parameter(parser):
- group = parser.add_mutually_exclusive_group()
- group.add_argument("-d", "--device", default="/dev/video0",
- help="Device for the webcam to use")
- group.add_argument("--highest-device", action="store_true",
- help=("Use the /dev/videoN "
- "where N is the highest value available"))
- group.add_argument("--lowest-device", action="store_true",
- help=("Use the /dev/videoN "
- "where N is the lowest value available"))
- subparsers.add_parser('detect')
- led_parser = subparsers.add_parser('led')
- add_device_parameter(led_parser)
- display_parser = subparsers.add_parser('display')
- add_device_parameter(display_parser)
- still_parser = subparsers.add_parser('still')
- add_device_parameter(still_parser)
- still_parser.add_argument("-f", "--filename",
- help="Filename to store the picture")
- still_parser.add_argument("-q", "--quiet", action="store_true",
- help=("Don't display picture, "
- "just write the picture to a file"))
- resolutions_parser = subparsers.add_parser('resolutions')
- add_device_parameter(resolutions_parser)
- args = parser.parse_args(argv)
-
- def get_video_devices():
- devices = sorted(glob('/dev/video[0-9]'),
- key=lambda d: re.search(r'\d', d).group(0))
- assert len(devices) > 0, "No video devices found"
- return devices
-
- if hasattr(args, 'highest_device') and args.highest_device:
- args.device = get_video_devices()[-1]
- elif hasattr(args, 'lowest_device') and args.lowest_device:
- args.device = get_video_devices()[0]
- return args
-
-
-if __name__ == "__main__":
- args = parse_arguments(sys.argv[1:])
-
- if not args.test:
- args.test = 'detect'
-
- logging.basicConfig(level=args.log_level)
-
- # Import Gst only for the test cases that will need it
- if args.test in ['display', 'still', 'led', 'resolutions']:
- from gi.repository import Gst
- if Gst.version()[0] > 0:
- gst_plugin = 'videoconvert'
- gst_video_type = 'video/x-raw'
- else:
- gst_plugin = 'ffmpegcolorspace'
- gst_video_type = 'video/x-raw-yuv'
- Gst.init(None)
- camera = CameraTest(args, gst_plugin, gst_video_type)
- else:
- camera = CameraTest(args)
-
- sys.exit(getattr(camera, args.test)())
diff --git a/bin/check-prerelease b/bin/check-prerelease
new file mode 100755
index 0000000..1f3d017
--- /dev/null
+++ b/bin/check-prerelease
@@ -0,0 +1,137 @@
+#!/usr/bin/env python3
+
+"""
+Script to test that the system is NOT running prerelease software
+
+Copyright (c) 2018 Canonical Ltd.
+
+Authors
+ Rod Smith <rod.smith@canonical.com>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License version 3,
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+The purpose of this script is to identify whether an EFI-based
+system booted from the network (test passes) or from a local disk
+(test fails).
+
+Usage:
+ check-prerelease
+"""
+
+import platform
+import shlex
+import sys
+
+from subprocess import Popen, PIPE
+
+
+def check_kernel_status():
+ """Check kernel to see if it's supported for certification
+
+ :returns:
+ True if OK, False if not
+ """
+ kernel_release = platform.release()
+
+ retval = False
+ command = "apt-cache show linux-image-{}".format(kernel_release)
+ aptinfo = []
+ aptinfo_bytes = (Popen(shlex.split(command), stdout=PIPE)
+ .communicate()[0])
+ aptinfo = (aptinfo_bytes.decode(encoding="utf-8", errors="ignore")
+ .splitlines())
+
+ """Kernel apt-cache info includes a 'Supported:' line on release
+ kernels to identify the period of support. This line is missing on
+ pre-release kernels. Thus, we return a True value only if this
+ line is present and shows a 5-year support period."""
+ if any("Supported: 5y" in s for s in aptinfo):
+ retval = True
+ if any("Supported: 9m" in s for s in aptinfo):
+ print("* Kernel is supported for 9 months; it is an interim release!")
+ if not any("Supported:" in s for s in aptinfo):
+ print("* Could not find a kernel support period; "
+ "may be a pre-release kernel!")
+
+ """We also want to exclude 'edge' kernels, which are identified via
+ the 'Source:' line in the apt-cache output...."""
+ if any("Source: linux-signed-hwe-edge" in s for s in aptinfo):
+ print("* Kernel is an 'edge' kernel!")
+ retval = False
+ if any("Source: linux-hwe-edge" in s for s in aptinfo):
+ print("* Kernel is an 'edge' kernel!")
+ retval = False
+
+ """We also want to exclude low-latency kernels, which are identified
+ via the kernel name string itself...."""
+ if "lowlatency" in kernel_release:
+ print("* Kernel is a low-latency kernel!")
+ retval = False
+
+ if (not retval):
+ print("* Kernel release is {}".format(kernel_release))
+ print("* Kernel is ineligible for certification!")
+
+ return retval
+
+
+def check_os_status():
+ """Check OS to see if it's supported for certification. Note that this
+ passes any release version (even a non-LTS version), but not pre-release
+ versions.
+
+ :returns:
+ True if OK, False if not
+ """
+ retval = True
+ command = "lsb_release -s -d"
+ lsbinfo = []
+ lsbinfo_bytes = (Popen(shlex.split(command), stdout=PIPE)
+ .communicate()[0])
+ lsbinfo = (lsbinfo_bytes.decode(encoding="utf-8", errors="ignore")
+ .rstrip())
+
+ """OS information include '(development branch)' on pre-release
+ installations. Such installations should fail this test."""
+ if "(development branch)" in lsbinfo:
+ print("* 'lsb_release -s -d' result is '{}'".format(lsbinfo))
+ print("* OS is reported as a development branch:")
+ print("* {}".format(lsbinfo))
+ retval = False
+ print("")
+
+ return retval
+
+
+def main():
+ """Check to see if the machine is running pre-release kernel or OS."""
+
+ retval = 0
+ if (not check_kernel_status()):
+ retval = 1
+ if (not check_os_status()):
+ retval += 2
+ if (retval == 0):
+ print("** All OK; production kernel and OS.")
+ elif (retval == 1):
+ print("** Test FAILS; running ineligible kernel!")
+ elif (retval == 2):
+ print("** Test FAILS; running pre-release OS!")
+ else:
+ print("** Test FAILS; running pre-release OS with ineligible kernel!")
+
+ return(retval)
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/bin/cycle_vts b/bin/cycle_vts
index 8b7f5bc..c5e1677 100755
--- a/bin/cycle_vts
+++ b/bin/cycle_vts
@@ -12,14 +12,14 @@ then
exit 1
fi
-if [ "$CURRENT_VT" -ne "1" ]
+if [ "$CURRENT_VT" == "7" ]
then
chvt 1
else
- chvt 2
+ chvt 3
fi
-sleep 2
+sleep 5
chvt "$CURRENT_VT"
sleep 2
diff --git a/bin/disk_info b/bin/disk_info
index 5efedbd..0b45443 100755
--- a/bin/disk_info
+++ b/bin/disk_info
@@ -53,7 +53,7 @@ def main():
disks = 0
for line in lsblk.splitlines():
m = pattern.match(line)
- if not m or m.group('TYPE') != 'disk':
+ if not m or m.group('TYPE') not in ('disk', 'crypt'):
continue
# Only consider MMC block devices if one of their mounted partitions is
# root (/)
diff --git a/bin/disk_read_performance_test b/bin/disk_read_performance_test
index e2a219d..00810a5 100755
--- a/bin/disk_read_performance_test
+++ b/bin/disk_read_performance_test
@@ -4,7 +4,7 @@
#
#Default to a lower bound of 15 MB/s
-DEFAULT_BUF_READ=15
+DEFAULT_BUF_READ=${DISK_READ_PERF:-15}
for disk in $@; do
diff --git a/bin/efi-pxeboot b/bin/efi-pxeboot
index 724fefb..24ac231 100755
--- a/bin/efi-pxeboot
+++ b/bin/efi-pxeboot
@@ -49,6 +49,7 @@ def discover_data():
.splitlines())
boot_entries = {}
boot_order = []
+ boot_current = ""
if len(bootinfo) > 1:
for s in bootinfo:
if "BootOrder" in s:
@@ -103,7 +104,7 @@ def is_pxe_booted(boot_entries, boot_order, boot_current):
or "refind_" in desc:
# This string indicates a boot directly from the normal Ubuntu GRUB
# or rEFInd installation on the hard disk.
- print("The system seems to have booted directly from the hard disk!")
+ print("FAIL: The system has booted directly from the hard disk!")
retval = 1
elif "SATA" in desc or "Sata" in desc or "Hard Drive" in desc:
# These strings indicate booting with a "generic" disk entry (one
diff --git a/bin/fde_tests.py b/bin/fde_tests.py
new file mode 100755
index 0000000..3008735
--- /dev/null
+++ b/bin/fde_tests.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python3
+# Copyright 2018 Canonical Ltd.
+# Written by:
+# Jonathan Cave <jonathan.cave@canonical.com>
+
+"""Test that Full Disk Encryption is in use.
+
+$ fde_tests.py
+Canonical has a reference implementation of full disk encryption for IoT
+devices. With no arguments passed this test checks this implementation is in
+operation on the device under test.
+
+$ fde_tests.py desktop
+Checks if the system appears to be using full disk encryption as configured by
+the desktop installer.
+"""
+
+import os
+import re
+import subprocess as sp
+import sys
+
+
+def main():
+ on_desktop = len(sys.argv) > 1 and sys.argv[1] == 'desktop'
+
+ # the mountpoint corresponding to the on disk encrypted partition
+ base_mount = '/' if on_desktop else '/writable'
+
+ # discover the underlying mount point for the encrypted part
+ cmd = 'findmnt {} -n -o SOURCE'.format(base_mount)
+ print('+ {}'.format(cmd))
+ try:
+ source = sp.check_output(cmd, shell=True).decode(
+ sys.stdout.encoding).strip()
+ except sp.CalledProcessError:
+ raise SystemExit(
+ 'ERROR: could not find mountpoint for {}'.format(base_mount))
+ print(source, '\n')
+
+ # resolve the source to an actual device node
+ print('+ realpath {}'.format(source))
+ device = os.path.realpath(source)
+ print(device, '\n')
+
+ # work upwards through the tree of devices until we find the one that has
+ # the type 'crypt'
+ kname = os.path.basename(device)
+ while True:
+ cmd = 'lsblk -r -n -i -o KNAME,TYPE,PKNAME | grep "^{}"'.format(kname)
+ print('+ {}'.format(cmd))
+ try:
+ lsblk = sp.check_output(cmd, shell=True).decode(
+ sys.stdout.encoding).strip()
+ except sp.CalledProcessError:
+ raise SystemExit('ERROR: lsblk call failed')
+ _, devtype, parent = lsblk.split(maxsplit=2)
+ print(devtype, '\n')
+ if devtype == 'crypt':
+ # found the device
+ break
+ if devtype == 'disk':
+ # reached the physical device, end the search
+ raise SystemExit(
+ 'ERROR: could not find a block device of type "crypt"')
+ kname = parent
+
+ # the presence of device with type 'crypt' is probably confirmation enough
+ # but to be really sure lets check to see it is found by cryptsetup
+
+ # first we need to know its mapper name
+ cmd = 'dmsetup info /dev/{} | grep "^Name:"'.format(kname)
+ print('+ {}'.format(cmd))
+ try:
+ mapper_name = sp.check_output(cmd, shell=True).decode(
+ sys.stdout.encoding).strip().split()[-1]
+ except sp.CalledProcessError:
+ raise SystemExit(
+ 'ERROR: dmsetup info on device {} failed'.format(kname))
+ print(mapper_name, '\n')
+
+ # then query the info in cryptsetup
+ cmd = 'cryptsetup status {}'.format(mapper_name)
+ print('+ {}'.format(cmd))
+ try:
+ cryptinfo = sp.check_output(cmd, shell=True).decode(
+ sys.stdout.encoding).strip()
+ except sp.CalledProcessError:
+ raise SystemExit('ERROR: dmsetup failed')
+ print(cryptinfo, '\n')
+
+ # use the type as the final arbiter of success
+ regexp = re.compile(r'type:\ *LUKS1')
+ if regexp.search(cryptinfo):
+ print('Full Disk Encryption is operational on this device')
+ else:
+ raise SystemExit('ERROR: cryptsetup did not report LUKS1 in use')
+
+
+if __name__ == "__main__":
+ main()
diff --git a/bin/gatt-notify-test.py b/bin/gatt-notify-test.py
new file mode 100755
index 0000000..9923a93
--- /dev/null
+++ b/bin/gatt-notify-test.py
@@ -0,0 +1,241 @@
+#!/usr/bin/env python3
+#
+# Copyright 2018 Canonical Ltd.
+# Written by:
+# Sylvain Pineau <sylvain.pineau@canonical.com>
+#
+# This is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3,
+# as published by the Free Software Foundation.
+#
+# This file is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this file. If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import logging
+import os
+import sys
+import time
+
+import dbus
+import dbus.service
+import dbus.mainloop.glib
+from gi.repository import GObject
+
+logger = logging.getLogger(__file__)
+logger.addHandler(logging.StreamHandler(sys.stdout))
+
+ADAPTER_INTERFACE = 'org.bluez.Adapter1'
+DEVICE_INTERFACE = 'org.bluez.Device1'
+PROP_INTERFACE = 'org.freedesktop.DBus.Properties'
+OM_INTERFACE = 'org.freedesktop.DBus.ObjectManager'
+GATT_SERVICE_INTERFACE = 'org.bluez.GattService1'
+GATT_CHRC_INTERFACE = 'org.bluez.GattCharacteristic1'
+
+dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+
+
+class BtAdapter:
+ """Bluetooth LE Adapter class."""
+ def __init__(self, pattern):
+ self._pattern = os.path.basename(pattern)
+ self._bus = dbus.SystemBus()
+ self._manager = dbus.Interface(
+ self._bus.get_object("org.bluez", "/"), OM_INTERFACE)
+ self._main_loop = GObject.MainLoop()
+ self._adapter = self._find_adapter()
+ self._path = self._adapter.object_path
+ self._props = dbus.Interface(self._adapter, PROP_INTERFACE)
+ self._name = self._props.Get(ADAPTER_INTERFACE, "Name")
+ self._addr = self._props.Get(ADAPTER_INTERFACE, "Address")
+ self._alias = self._props.Get(ADAPTER_INTERFACE, "Alias")
+ logger.info('Adapter found: [ {} ] {} - {}'.format(
+ self._path, self._addr, self._alias))
+
+ def _get_managed_objects(self):
+ return self._manager.GetManagedObjects()
+
+ def _find_adapter(self):
+ for path, ifaces in self._get_managed_objects().items():
+ adapter = ifaces.get(ADAPTER_INTERFACE)
+ if adapter is None:
+ continue
+ if (self._pattern == adapter["Address"] or
+ path.endswith(self._pattern)):
+ obj = self._bus.get_object("org.bluez", path)
+ return dbus.Interface(obj, ADAPTER_INTERFACE)
+ raise SystemExit("Bluetooth adapter not found!")
+
+ def ensure_powered(self):
+ """Turn the adapter on."""
+ self._props.Set(ADAPTER_INTERFACE, "Powered", dbus.Boolean(1))
+ logger.info('Adapter powered on')
+
+ def scan(self, timeout=10):
+ """Scan for BT devices."""
+ dbus.Interface(self._adapter, ADAPTER_INTERFACE).StartDiscovery()
+ logger.info('Adapter scan on ({}s)'.format(timeout))
+ GObject.timeout_add_seconds(timeout, self._scan_timeout)
+ self._main_loop.run()
+
+ def _scan_timeout(self):
+ dbus.Interface(self._adapter, ADAPTER_INTERFACE).StopDiscovery()
+ logger.info('Adapter scan completed')
+ self._main_loop.quit()
+
+ def find_device_with_service(self, ADV_SVC_UUID):
+ """Find a device with a given remote service."""
+ for path, ifaces in self._get_managed_objects().items():
+ device = ifaces.get(DEVICE_INTERFACE)
+ if device is None:
+ continue
+ logger.debug("{} {} {}".format(
+ path, device["Address"], device["Alias"]))
+ if ADV_SVC_UUID in device["UUIDs"] and path.startswith(self._path):
+ obj = self._bus.get_object("org.bluez", path)
+ logger.info('Device found: [ {} ] {} - {}'.format(
+ path, device["Name"], device["Address"]))
+ return dbus.Interface(obj, DEVICE_INTERFACE)
+ raise SystemExit("Bluetooth device not found!")
+
+ def remove_device(self, device):
+ """Remove the remote device object at the given path."""
+ try:
+ self._adapter.RemoveDevice(device)
+ except dbus.exceptions.DBusException as msg:
+ logging.error(msg)
+ raise SystemExit(1)
+ logger.info('Device properly removed')
+
+
+class BtGATTRemoteService:
+ """Bluetooth LE GATT Remote Service class."""
+ def __init__(self, SVC_UUID, adapter, device, max_notif):
+ self.SVC_UUID = SVC_UUID
+ self._adapter = adapter
+ self.device = device
+ self._max_notif = max_notif
+ self._notifications = 0
+ self._bus = dbus.SystemBus()
+ self._manager = dbus.Interface(
+ self._bus.get_object("org.bluez", "/"), OM_INTERFACE)
+ self._main_loop = GObject.MainLoop()
+ self._service = self._find_service()
+ self._path = self._service.object_path
+
+ def _get_managed_objects(self):
+ return self._manager.GetManagedObjects()
+
+ def _find_service(self):
+ for path, ifaces in self._get_managed_objects().items():
+ if GATT_SERVICE_INTERFACE not in ifaces.keys():
+ continue
+ service = self._bus.get_object('org.bluez', path)
+ props = dbus.Interface(service, PROP_INTERFACE)
+ if props.Get(GATT_SERVICE_INTERFACE, "UUID") == self.SVC_UUID:
+ logger.info('Service found: {}'.format(path))
+ return service
+ self._adapter.remove_device(self._device)
+ raise SystemExit("Bluetooth Service not found!")
+
+ def find_chrc(self, MSRMT_UUID):
+ for path, ifaces in self._get_managed_objects().items():
+ if GATT_CHRC_INTERFACE not in ifaces.keys():
+ continue
+ chrc = self._bus.get_object('org.bluez', path)
+ props = dbus.Interface(chrc, PROP_INTERFACE)
+ if props.Get(GATT_CHRC_INTERFACE, "UUID") == MSRMT_UUID:
+ logger.info('Characteristic found: {}'.format(path))
+ return chrc
+ self._adapter.remove_device(self._device)
+ raise SystemExit("Bluetooth Characteristic not found!")
+
+ def _generic_error_cb(self, error):
+ self._adapter.remove_device(self._device)
+ self._main_loop.quit()
+ raise SystemExit('D-Bus call failed: ' + str(error))
+
+ def _start_notify_cb(self):
+ logger.info('Notifications enabled')
+
+ def _notify_timeout(self):
+ self._adapter.remove_device(self._device)
+ self._main_loop.quit()
+ raise SystemExit('Notification test failed')
+
+ def _changed_cb(self, iface, changed_props, invalidated_props):
+ if iface != GATT_CHRC_INTERFACE:
+ return
+ if not len(changed_props):
+ return
+ value = changed_props.get('Value', None)
+ if not value:
+ return
+ logger.debug('New Notification')
+ self._notifications += 1
+ if self._notifications >= self._max_notif:
+ logger.info('Notification test succeeded')
+ self._main_loop.quit()
+
+ def check_notification(self, chrc, timeout=20):
+ # Listen to PropertiesChanged signals from the BLE Measurement
+ # Characteristic.
+ prop_iface = dbus.Interface(chrc, PROP_INTERFACE)
+ prop_iface.connect_to_signal("PropertiesChanged", self._changed_cb)
+
+ # Subscribe to BLE Measurement notifications.
+ chrc.StartNotify(reply_handler=self._start_notify_cb,
+ error_handler=self._generic_error_cb,
+ dbus_interface=GATT_CHRC_INTERFACE)
+ GObject.timeout_add_seconds(timeout, self._notify_timeout)
+ self._main_loop.run()
+
+
+def main():
+ logger.setLevel(logging.DEBUG)
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "id",
+ help='Address, udev path or name (hciX) of the BT adapter')
+ parser.add_argument(
+ "ADV_SVC_UUID", help='Beacon Gatt configuration service UUID')
+ parser.add_argument(
+ "SVC_UUID", help='Beacon Gatt notification service UUID')
+ parser.add_argument("MSRMT_UUID", help='Beacon Gatt measurement UUID')
+ parser.add_argument(
+ "--max-notif", "-m", type=int, default=5,
+ help="Maximum notification threshold")
+ args = parser.parse_args()
+ adapter = BtAdapter(args.id)
+ adapter.ensure_powered()
+ adapter.scan()
+ device = adapter.find_device_with_service(args.ADV_SVC_UUID)
+ try:
+ device.Connect()
+ except dbus.exceptions.DBusException as msg:
+ logging.error(msg)
+ adapter.remove_device(device)
+ raise SystemExit(1)
+ logger.info('Device connected, waiting 10s for services to be available')
+ time.sleep(10) # Let all the services to broadcast their UUIDs
+ service = BtGATTRemoteService(
+ args.SVC_UUID, adapter, device, args.max_notif)
+ chrc = service.find_chrc(args.MSRMT_UUID)
+ service.check_notification(chrc)
+ try:
+ device.Disconnect()
+ except dbus.exceptions.DBusException as msg:
+ logging.error(msg)
+ adapter.remove_device(device)
+ raise SystemExit(1)
+ logger.info('Device properly disconnected')
+ adapter.remove_device(device)
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/bin/gpu_test b/bin/gpu_test
index bd22acc..49ea31a 100755
--- a/bin/gpu_test
+++ b/bin/gpu_test
@@ -23,11 +23,13 @@ lockups.
Inspired by the workload directory of the xdiagnose package.
"""
+import gi
import os
import re
import subprocess
import sys
import time
+gi.require_version('Gio', '2.0')
from gi.repository import Gio
from math import cos, sin
from threading import Thread
diff --git a/bin/graphics_driver b/bin/graphics_driver
index dfb5522..6d28f5a 100755
--- a/bin/graphics_driver
+++ b/bin/graphics_driver
@@ -361,7 +361,10 @@ def hybrid_graphics_check(xlog):
def main():
- xlog = XorgLog("/var/log/Xorg.0.log")
+ if os.path.isfile("/var/log/Xorg.0.log"):
+ xlog = XorgLog("/var/log/Xorg.0.log")
+ else:
+ xlog = XorgLog(os.path.expanduser("~/.local/share/xorg/Xorg.0.log"))
results = []
results.append(get_driver_info(xlog))
diff --git a/bin/graphics_env b/bin/graphics_env
index 9511560..89c761d 100755
--- a/bin/graphics_env
+++ b/bin/graphics_env
@@ -19,7 +19,7 @@ if [[ $DRIVER == "amdgpu" || $DRIVER == "radeon" ]]; then
if [ $INDEX -gt 1 ]; then
# See https://wiki.archlinux.org/index.php/PRIME
echo "Setting up PRIME GPU offloading for AMD discrete GPU"
- if ! grep -q DRI3 /var/log/Xorg.0.log; then
+ if ! cat /var/log/Xorg.0.log ~/.local/share/xorg/Xorg.0.log 2>&1 | grep -q DRI3; then
PROVIDER_ID=`xrandr --listproviders | grep "Sink Output" | awk {'print $4'} | tail -1`
SINK_ID=`xrandr --listproviders | grep "Source Output" | awk {'print $4'} | tail -1`
xrandr --setprovideroffloadsink ${PROVIDER_ID} ${SINK_ID}
diff --git a/bin/gst_pipeline_test b/bin/gst_pipeline_test
index cde7fbb..4d9f3c4 100755
--- a/bin/gst_pipeline_test
+++ b/bin/gst_pipeline_test
@@ -1,11 +1,14 @@
#!/usr/bin/env python3
from argparse import ArgumentParser
+import gi
import logging
import re
import os
import sys
import time
+gi.require_version('Gst', '1.0')
+gi.require_version('GLib', '2.0')
from gi.repository import Gst
from gi.repository import GLib
from subprocess import check_output
diff --git a/bin/ipmi_test b/bin/ipmi_test
index 8be8b96..71e7f36 100755
--- a/bin/ipmi_test
+++ b/bin/ipmi_test
@@ -28,13 +28,15 @@ done
echo
echo "Checking for chassis status"
ipmitool chassis status && echo "Successfully got chassis status" && chassis=0 || chassis=1
-echo "Checking to see if we can get sensor data"
-ipmitool sdr list full && echo "Successfully got sensor data" && sensor=0 || sensor=1
+echo "Checking to see if we can get power status"
+ipmitool power status && echo "Successfully got power status" && power=0 || power=1
+echo "Checking to see if we can get user data"
+ipmitool user list && echo "Successfully got user data" && user=0 || user=1
echo "Checking to see if we can get info on the BMC"
ipmitool bmc info && echo "Successfully got BMC information" && bmc=0 || bmc=1
# if everything passes, exit 0
-[ $chassis -eq 0 ] && [ $sensor -eq 0 ] && [ $bmc -eq 0 ] && exit 0 || echo "FAILURE: chassis: $chassis sensor: $sensor bmc: $bmc"
+[ $chassis -eq 0 ] && [ $power -eq 0 ] && [ $user -eq 0 ] && [ $bmc -eq 0 ] && exit 0 || echo "FAILURE: chassis: $chassis power: $power user: $user bmc: $bmc"
# otherwise exit 1
exit 1
diff --git a/bin/key_test b/bin/key_test
index 3cf7ae0..2bd95f9 100755
--- a/bin/key_test
+++ b/bin/key_test
@@ -18,6 +18,7 @@
# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
+import gi
import os
import sys
@@ -27,6 +28,7 @@ import struct
import termios
from gettext import gettext as _
+
from gi.repository import GObject
from optparse import OptionParser
@@ -221,6 +223,8 @@ class GtkReporter(Reporter):
def __init__(self, *args, **kwargs):
super(GtkReporter, self).__init__(*args, **kwargs)
+ gi.require_version('Gdk', '3.0')
+ gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, Gtk
# Initialize GTK constants
diff --git a/bin/keyboard_test b/bin/keyboard_test
index 309b845..1615106 100755
--- a/bin/keyboard_test
+++ b/bin/keyboard_test
@@ -33,8 +33,10 @@ def cli_prompt():
def gtk_prompt():
+ import gi
+ gi.require_version('Gdk', '3.0')
+ gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk
-
# create a new window
window = Gtk.Window()
window.set_type_hint(Gdk.WindowType.TOPLEVEL)
diff --git a/bin/lock_screen_watcher b/bin/lock_screen_watcher
index 4e9847e..1c30270 100755
--- a/bin/lock_screen_watcher
+++ b/bin/lock_screen_watcher
@@ -17,7 +17,9 @@
# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
import dbus
+import gi
from dbus.mainloop.glib import DBusGMainLoop
+gi.require_version('GLib', '2.0')
from gi.repository import GObject
from gi.repository import GLib
diff --git a/bin/manage_compiz_plugin b/bin/manage_compiz_plugin
index a1236ea..9650a9d 100755
--- a/bin/manage_compiz_plugin
+++ b/bin/manage_compiz_plugin
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# This file is part of Checkbox.
#
-# Copyright 2014-2015 Canonical Ltd.
+# Copyright 2014-2018 Canonical Ltd.
# Written by:
# Daniel Manrique <roadmr@ubuntu.com>
# Sylvain Pineau <sylvain.pineau@canonical.com>
@@ -33,14 +33,13 @@ import sys
import subprocess
import time
-PATH="org.compiz.core:/org/compiz/profiles/unity/plugins/core/"
-KEY="active-plugins"
+KEY="/org/compiz/profiles/unity/plugins/core/active-plugins"
gettext.textdomain("com.canonical.certification.checkbox")
gettext.bindtextdomain("com.canonical.certification.checkbox",
os.getenv("CHECKBOX_PROVIDER_LOCALE_DIR", None))
-plugins = eval(subprocess.check_output(["gsettings", "get", PATH, KEY]))
+plugins = eval(subprocess.check_output(["dconf", "read", KEY]))
parser = argparse.ArgumentParser(description=_("enable/disable compiz plugins"),
epilog=_("Available plugins: {}").format(plugins))
@@ -58,6 +57,6 @@ else:
if args.plugin not in plugins:
raise SystemExit(_("Plugin {} doesn't exist").format(args.plugin))
plugins.remove(args.plugin)
-subprocess.call(["gsettings", "set", PATH, KEY, str(plugins)])
+subprocess.call(["dconf", "write", KEY, str(plugins)])
time.sleep(3)
diff --git a/bin/net_if_watcher.py b/bin/net_if_watcher.py
new file mode 100755
index 0000000..0ba61df
--- /dev/null
+++ b/bin/net_if_watcher.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python3
+# Copyright 2018 Canonical Ltd.
+# All rights reserved.
+#
+# Written by:
+# Maciej Kisielewski <maciej.kisielewski@canonical.com>
+"""
+Detect insertion of a new network interface.
+"""
+
+from pathlib import Path
+import time
+
+
+def get_ifaces():
+ return set([i.name for i in Path("/sys/class/net").iterdir()])
+
+
+def main():
+ print("INSERT NOW")
+ starting_ifaces = get_ifaces()
+ attempts = 20
+ while attempts > 0:
+ now_ifaces = get_ifaces()
+ # check if something disappeared
+ if not starting_ifaces == now_ifaces & starting_ifaces:
+ raise SystemExit("Interface(s) disappeared: {}".format(
+ ", ".join(list(starting_ifaces - now_ifaces))))
+ new_ifaces = now_ifaces - starting_ifaces
+ if new_ifaces:
+ print()
+ print("New interface(s) detected: {}".format(
+ ", ".join(list(new_ifaces))))
+ return
+ time.sleep(1)
+ print('.', end='', flush=True)
+ attempts -= 1
+ print()
+ raise SystemExit("Failed to detect new network interface")
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/network_restart b/bin/network_restart
index 31f1da7..d01e983 100755
--- a/bin/network_restart
+++ b/bin/network_restart
@@ -14,6 +14,9 @@ from subprocess import check_output, check_call, CalledProcessError, STDOUT
from argparse import ArgumentParser
try:
+ import gi
+ gi.require_version('GLib', '2.0')
+ gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GObject, GLib
GLib.threads_init()
GObject.threads_init()
diff --git a/bin/pm_test b/bin/pm_test
index d1bd3b1..70cd121 100755
--- a/bin/pm_test
+++ b/bin/pm_test
@@ -1,4 +1,10 @@
#!/usr/bin/env python3
+"""
+If you're debugging this program, set PM_TEST_DRY_RUN in your environment.
+It will make the script not run actual S3, S4, reboot and poweroff commands.
+"""
+import gi
+import json
import logging
import logging.handlers
import os
@@ -13,7 +19,7 @@ from calendar import timegm
from configparser import ConfigParser
from datetime import datetime, timedelta
from time import localtime, time
-
+gi.require_version("Gtk", "3.0")
from gi.repository import GObject, Gtk
@@ -41,8 +47,13 @@ def main():
logging.debug('Arguments: {0!r}'.format(args))
logging.debug('Extra Arguments: {0!r}'.format(extra_args))
+ dry_run = os.environ.get('PM_TEST_DRY_RUN', False)
+ if dry_run:
+ logging.info("Running in dry-run mode")
+
try:
- operation = PowerManagementOperation(args, extra_args, user=username)
+ operation = PowerManagementOperation(
+ args, extra_args, user=username, dry_run=dry_run)
operation.setup()
operation.run()
except (TestCancelled, TestFailed) as exception:
@@ -64,10 +75,11 @@ def main():
class PowerManagementOperation():
SLEEP_TIME = 5
- def __init__(self, args, extra_args, user=None):
+ def __init__(self, args, extra_args, user=None, dry_run=False):
self.args = args
self.extra_args = extra_args
self.user = user
+ self.dry_run = dry_run
def setup(self):
"""
@@ -128,11 +140,17 @@ class PowerManagementOperation():
logging.info('Executing new {0!r} operation...'
.format(self.args.pm_operation))
logging.debug('Executing: {0!r}...'.format(command_str))
- # The PM operation is performed asynchronously so let's just wait
- # indefinitely until it happens and someone comes along to kill us.
- # This addresses LP: #1413134
- subprocess.check_call(command_str, shell=True)
- signal.pause()
+ if self.dry_run:
+ print("\n\nRUNNING IN DRY-RUN MODE")
+ print("Normally the program would run: {}".format(command_str))
+ print("Waiting for Enter instead")
+ input()
+ else:
+ # The PM operation is performed asynchronously so let's just wait
+ # indefinitely until it happens and someone comes along to kill us.
+ # This addresses LP: #1413134
+ subprocess.check_call(command_str, shell=True)
+ signal.pause()
def run_suspend_cycles(self, cycles_count, fwts):
"""Run suspend and resume cycles."""
@@ -155,15 +173,21 @@ class PowerManagementOperation():
command_str = command_tpl.format(script_path, cycles_count)
logging.info('Running suspend/resume cycles')
logging.debug('Executing: {0!r}...'.format(command_str))
- try:
- # We call sleep_test or fwts_test script and log its output as it
- # contains average times we need to compute global average times
- # later.
- logging.info(subprocess.check_output(
- command_str, universal_newlines=True, shell=True))
- except subprocess.CalledProcessError as e:
- logging.error('Error while running {0}:'.format(e.cmd))
- logging.error(e.output)
+ if self.dry_run:
+ print("\n\nRUNNING IN DRY-RUN MODE")
+ print("Normally the program would run: {}".format(command_str))
+ print("Waiting for Enter instead")
+ input()
+ else:
+ try:
+ # We call sleep_test or fwts_test script and log its output as
+ # it contains average times we need to compute global average
+ # times later.
+ logging.info(subprocess.check_output(
+ command_str, universal_newlines=True, shell=True))
+ except subprocess.CalledProcessError as e:
+ logging.error('Error while running {0}:'.format(e.cmd))
+ logging.error(e.output)
def summary(self):
"""
@@ -199,6 +223,29 @@ class PowerManagementOperation():
message = ('{0} test complete'
.format(self.args.pm_operation.capitalize()))
+ total_suspends_expected = (
+ self.args.suspends_before_reboot * self.args.total)
+ problems = ''
+ fwts_log_path = os.path.join(self.args.log_dir, 'fwts.log')
+ try:
+ with open(fwts_log_path, 'rt') as f:
+ magic_line = 'Completed S3 cycle(s) \n'
+ count = f.readlines().count(magic_line)
+ if count != total_suspends_expected:
+ problems = (
+ "Found {} occurrences of '{}'. Expected {}".format(
+ count, magic_line.strip(),
+ total_suspends_expected))
+ except FileNotFoundError:
+ problems = "Error opening {}".format(fwts_log_path)
+ if problems:
+ result = {
+ 'outcome': 'fail' if problems else 'pass',
+ 'comment': problems,
+ }
+ with open(os.path.join(self.args.log_dir, '__result'), 'wt') as f:
+ json.dump(result, f)
+
if self.args.silent:
logging.info(message)
else:
@@ -206,6 +253,8 @@ class PowerManagementOperation():
MessageDialog(title, message).run()
if self.args.checkbox_respawn_cmd:
subprocess.run(
+ r'unset LD_LIBRARY_PATH;'
+ r'unset PYTHONPATH; unset PYTHONHOME;'
r'DISPLAY=:0 x-terminal-emulator -e "sudo -u '
r'{} bash -c \"source {}; exec bash\""'.format(
self.user, self.args.checkbox_respawn_cmd), shell=True)
@@ -572,11 +621,11 @@ autologin-user-timeout=0
shutil.copystat(self.config_filename, backup_filename)
break
- with open(self.config_filename, 'w') as f:
- if self.config_filename == '/etc/lightdm/lightdm.conf':
- f.write(self.template.format(username=self.user))
- elif self.config_filename == '/etc/gdm3/custom.conf':
- self.parser.write(f)
+ with open(self.config_filename, 'w') as f:
+ if self.config_filename == '/etc/lightdm/lightdm.conf':
+ f.write(self.template.format(username=self.user))
+ elif self.config_filename == '/etc/gdm3/custom.conf':
+ self.parser.write(f)
def disable(self):
"""
diff --git a/bin/removable_storage_test b/bin/removable_storage_test
index 42e197a..6de3f97 100755
--- a/bin/removable_storage_test
+++ b/bin/removable_storage_test
@@ -841,20 +841,10 @@ def main():
# This will raise KeyError for no
# associated disk device was found.
xhci_disks = test.get_disks_xhci()
- # pep8 style suggest to limit the try clause
- # to the absolute minimum amount of code necessary
- try:
- disk_xhci_flag = xhci_disks[disk]
- except KeyError:
- print("\t\tDisk does not use xhci_hci.")
- return 1
- else:
- if('xhci' == disk_xhci_flag):
- print("\t\tDriver Detected: xhci_hcd")
- else:
- print("\t\tDisk does not use xhci_hci.")
- logging.debug("disk_xhci_flag is not xhci")
- return 1
+ if test.get_disks_xhci().get(disk, '') != 'xhci':
+ raise SystemExit(
+ "\t\tDisk does not use xhci_hcd.")
+ print("\t\tDriver Detected: xhci_hcd")
else:
# Give it a hint for the detection failure.
# LP: #1362902
diff --git a/bin/resolution_test b/bin/resolution_test
index a50868b..55a7828 100755
--- a/bin/resolution_test
+++ b/bin/resolution_test
@@ -1,9 +1,10 @@
#!/usr/bin/env python3
+import gi
import sys
from argparse import ArgumentParser
-
+gi.require_version('Gdk', '3.0')
from gi.repository import Gdk
diff --git a/bin/socketcan_test.py b/bin/socketcan_test.py
new file mode 100755
index 0000000..232fc2c
--- /dev/null
+++ b/bin/socketcan_test.py
@@ -0,0 +1,199 @@
+#!/usr/bin/env python3
+# This file is part of Checkbox.
+#
+# Copyright 2018 Canonical Ltd.
+# Written by:
+# Jonathan Cave <jonathan.cave@canonical.com>
+#
+# Checkbox is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3,
+# as published by the Free Software Foundation.
+#
+# Checkbox is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import os
+import socket
+import struct
+import sys
+import textwrap
+import threading
+import time
+
+
+class CANSocket():
+
+ # struct module format strings for CAN packets
+ # Normal format:
+ # < little-endian
+ # I unsigned int (4) : CAN-ID + EFF/RTR/ERR Flags
+ # B unsigned char (1) : Data length
+ # 3x padding (3 * 1) : -
+ # 8s char array (8 * 1) : Data
+ FORMAT = "<IB3x8s"
+ # Flexible Data (FD) rate format:
+ # < little-endian
+ # I unsigned int (4) : CAN-ID + EFF/RTR/ERR Flags
+ # B unsigned char (1) : Data length
+ # B unsigned char (1) : FD Flags
+ # 2x padding (2 * 1) : -
+ # 64s char array (64 * 1) : Data
+ FD_FORMAT = "<IBB2x64s"
+
+ CAN_MTU = struct.Struct(FORMAT).size
+ CANFD_MTU = struct.Struct(FD_FORMAT).size
+
+ # Socket options from <linux/can/raw.h>
+ CAN_RAW_FILTER = 1 # set 0 .. n can_filter(s)
+ CAN_RAW_ERR_FILTER = 2 # set filter for error frames
+ CAN_RAW_LOOPBACK = 3 # local loopback (default:on)
+ CAN_RAW_RECV_OWN_MSGS = 4 # receive my own msgs (default:off)
+ CAN_RAW_FD_FRAMES = 5 # allow CAN FD frames (default:off)
+ CAN_RAW_JOIN_FILTERS = 6 # all filters must match to trigger
+
+ def __init__(self, interface=None, fdmode=False, loopback=True):
+ self.sock = socket.socket(socket.PF_CAN, # protocol family
+ socket.SOCK_RAW,
+ socket.CAN_RAW)
+ self._fdmode = fdmode
+ self._loopback = loopback
+ if interface is not None:
+ self._bind(interface)
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args):
+ self.close()
+
+ def close(self):
+ self.sock.close()
+
+ def _bind(self, interface):
+ self.sock.bind((interface,))
+ if self._fdmode: # default is off
+ self.sock.setsockopt(socket.SOL_CAN_RAW, self.CAN_RAW_FD_FRAMES, 1)
+ if not self._loopback: # default is on
+ self.sock.setsockopt(socket.SOL_CAN_RAW, self.CAN_RAW_LOOPBACK, 0)
+
+ def send(self, can_id, data, id_flags=0, fd_flags=0):
+ can_id = can_id | id_flags
+ if self._fdmode:
+ can_pkt = struct.pack(self.FD_FORMAT, can_id, len(data), fd_flags,
+ data)
+ else:
+ can_pkt = struct.pack(self.FORMAT, can_id, len(data), data)
+ self.sock.send(can_pkt)
+
+ def recv(self):
+ if self._fdmode:
+ can_pkt = self.sock.recv(self.CANFD_MTU)
+ can_id, length, fd_flags, data = struct.unpack(self.FD_FORMAT,
+ can_pkt)
+ else:
+ can_pkt = self.sock.recv(self.CAN_MTU)
+ can_id, length, data = struct.unpack(self.FORMAT, can_pkt)
+ can_id &= socket.CAN_EFF_MASK
+ return (can_id, data[:length])
+
+
+def echo_test(args):
+ # ID conversion and size check
+ print('Using source ID: {}'.format(args.can_id))
+ can_id_i = int(args.can_id, 16)
+ if can_id_i > 2047 and not args.effid:
+ raise SystemExit('ERROR: CAN ID to high for SFF')
+ id_flags = 0
+ if args.effid:
+ print('Setting EFF CAN ID flag')
+ id_flags = socket.CAN_EFF_FLAG
+
+ # Whether to enable local loopback, required for local only test
+ # but only want to parse packets from other end if remote
+ loopback = not args.remote
+
+ # Default data size is 8 bytes but if testing FD Mode use 64
+ data_size = 8
+ if args.fdmode:
+ data_size = 64
+ data_b = os.urandom(data_size)
+ print('Sending data: {}'.format(data_b.hex()))
+
+ recv_id_i = None
+ recv_data_b = None
+
+ def receive():
+ nonlocal recv_id_i
+ nonlocal recv_data_b
+ print('Opening read socket on {}'.format(args.interface))
+ with CANSocket(args.interface, fdmode=args.fdmode,
+ loopback=loopback) as recv_s:
+ recv_id_i, recv_data_b = recv_s.recv()
+
+ # Create a receive thread
+ recv_t = threading.Thread(target=receive, daemon=True)
+ recv_t.start()
+ time.sleep(1)
+
+ print('Opening send socket on {}'.format(args.interface))
+ # Open socket, will raise OSError on failure
+ with CANSocket(args.interface, fdmode=args.fdmode,
+ loopback=loopback) as send_s:
+ print('Sending data...', flush=True)
+ try:
+ send_s.send(can_id_i, data_b, id_flags=id_flags)
+ except OSError as e:
+ print(e, file=sys.stderr)
+ if e.errno == 90:
+ raise SystemExit('ERROR: interface does not support FD Mode')
+ else:
+ raise SystemExit('ERROR: OSError on attempt to send')
+
+ recv_t.join(10)
+ if recv_t.is_alive():
+ raise SystemExit('ERROR: Timeout waiting to receive data')
+
+ print('Received packet')
+ print(' ID : {:x}'.format(recv_id_i))
+ print(' Data: {}'.format(recv_data_b.hex()))
+ if recv_id_i != can_id_i or recv_data_b != data_b:
+ raise SystemExit('ERROR: ID/Data received does not match sent')
+
+ print('\nPASSED')
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ description='SocketCAN Tests',
+ epilog=textwrap.dedent('''
+ Examples:
+ socketcan_test.py can0 123
+ socketcan_test.py can0 212 --remote
+ socketcan_test.py can0 FA123 --effid
+ socketcan_test.py can0 E407DB --effid --fdmode''').lstrip())
+ parser.add_argument('interface', type=str, help='Interface name e.g. can0')
+ parser.add_argument('can_id', type=str, help=textwrap.dedent('''
+ CAN ID of source in Hex, max of 11 bits using Standard Frame
+ Format (SFF). Specifying use of Extended Frame Format (EFF)
+ allows the use of up to 29 bit IDs.''').lstrip())
+ parser.add_argument('--remote', action='store_true',
+ help='Expect a remote device to echo the test packet')
+ parser.add_argument('--effid', action='store_true',
+ help='Use EFF ID (CAN 2.0 B)')
+ parser.add_argument('--fdmode', action='store_true',
+ help='Attempt to send 64 bytes of data i.e. FD mode')
+ parser.set_defaults(func=echo_test)
+
+ args = parser.parse_args()
+ args.func(args)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/bin/touchpad_test b/bin/touchpad_test
index 7152745..bc6a474 100755
--- a/bin/touchpad_test
+++ b/bin/touchpad_test
@@ -1,9 +1,13 @@
#!/usr/bin/env python3
+import gi
import sys
import gettext
from gettext import gettext as _
+gi.require_version('Gdk', '3.0')
+gi.require_version('Gio', '2.0')
+gi.require_version("Gtk", "3.0")
from gi.repository import Gio, Gtk, Gdk
from optparse import OptionParser
diff --git a/bin/xrandr_cycle b/bin/xrandr_cycle
index b9f615b..34f953c 100755
--- a/bin/xrandr_cycle
+++ b/bin/xrandr_cycle
@@ -87,6 +87,13 @@ for adapter, mode in modes:
highest_modes = []
for adapter, params in top_res_per_aspect.items():
for aspect, width in params.items():
+ # xrandr can list modes that are unsupported, unity-control-center
+ # defines minimum width and height, below which the resolution
+ # is not listed as a choice in display settings panel in UCC
+ # see should_show_resolution function in cc-display-panel.c
+ # from lp:unity-control-center
+ if width < 675 or width / aspect < 530:
+ continue
mode = '{}x{}'.format(width, width/aspect)
highest_modes.append((adapter, mode))