diff options
-rw-r--r-- | qutebrowser/misc/backendproblem.py | 16 | ||||
-rw-r--r-- | qutebrowser/utils/utils.py | 15 | ||||
-rw-r--r-- | qutebrowser/utils/version.py | 70 | ||||
-rw-r--r-- | tests/conftest.py | 6 | ||||
-rw-r--r-- | tests/unit/utils/test_version.py | 82 |
5 files changed, 159 insertions, 30 deletions
diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index 2ed273547..73044cd99 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -23,8 +23,6 @@ import os import sys import functools import html -import ctypes -import ctypes.util import enum import shutil import typing @@ -201,19 +199,10 @@ class _BackendProblemChecker: def _nvidia_shader_workaround(self) -> None: """Work around QOpenGLShaderProgram issues. - NOTE: This needs to be called before _handle_nouveau_graphics, or some - setups will segfault in version.opengl_vendor(). - See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826 """ self._assert_backend(usertypes.Backend.QtWebEngine) - - if os.environ.get('QUTE_SKIP_LIBGL_WORKAROUND'): - return - - libgl = ctypes.util.find_library("GL") - if libgl is not None: - ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL) + utils.libgl_workaround() def _handle_nouveau_graphics(self) -> None: """Force software rendering when using the Nouveau driver. @@ -231,7 +220,8 @@ class _BackendProblemChecker: if qtutils.version_check('5.10', compiled=False): return - if version.opengl_vendor() != 'nouveau': + opengl_info = version.opengl_info() + if opengl_info is None or opengl_info.vendor != 'nouveau': return if (os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1' or diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 368cb0ab6..39d46add8 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -36,6 +36,8 @@ import shlex import glob import mimetypes import typing +import ctypes +import ctypes.util from PyQt5.QtCore import QUrl from PyQt5.QtGui import QColor, QClipboard, QDesktopServices @@ -776,3 +778,16 @@ def ceil_log(number: int, base: int) -> int: result += 1 accum *= base return result + + +def libgl_workaround(): + """Work around QOpenGLShaderProgram issues, especially for Nvidia. + + See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826 + """ + if os.environ.get('QUTE_SKIP_LIBGL_WORKAROUND'): + return + + libgl = ctypes.util.find_library("GL") + if libgl is not None: + ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL) diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 1ad8b22cf..8dc4ec593 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -31,6 +31,7 @@ import enum import datetime import getpass import typing +import functools import attr import pkg_resources @@ -442,8 +443,8 @@ def version() -> str: if qapp: style = qapp.style() lines.append('Style: {}'.format(style.metaObject().className())) - platform_name = qapp.platformName() - lines.append('Platform plugin: {}'.format(platform_name)) + lines.append('Platform plugin: {}'.format(qapp.platformName())) + lines.append('OpenGL: {}'.format(opengl_info())) importpath = os.path.dirname(os.path.abspath(qutebrowser.__file__)) @@ -487,7 +488,55 @@ def version() -> str: return '\n'.join(lines) -def opengl_vendor() -> typing.Optional[str]: # pragma: no cover +@attr.s +class OpenGLInfo: + + """Information about the OpenGL setup in use.""" + + # If we're using OpenGL ES. If so, no further information is available. + gles = attr.ib(False) # type: bool + + # The name of the vendor. Examples: + # - nouveau + # - "Intel Open Source Technology Center", "Intel", "Intel Inc." + vendor = attr.ib(None) # type: typing.Optional[str] + + # The OpenGL version as a string. See tests for examples. + version_str = attr.ib(None) # type: typing.Optional[str] + + # The parsed version as a (major, minor) tuple of ints + version = attr.ib(None) # type: typing.Optional[typing.Tuple[int, ...]] + + # The vendor specific information following the version number + vendor_specific = attr.ib(None) # type: typing.Optional[str] + + def __str__(self) -> str: + if self.gles: + return 'OpenGL ES' + return '{}, {}'.format(self.vendor, self.version_str) + + @classmethod + def parse(cls, *, vendor: str, version: str) -> 'OpenGLInfo': + if ' ' not in version: + log.misc.warning("Failed to parse OpenGL version (missing space): " + "{}".format(version)) + return cls(vendor=vendor, version_str=version) + + num_str, vendor_specific = version.split(' ', maxsplit=1) + + try: + parsed_version = tuple(int(i) for i in num_str.split('.')) + except ValueError: + log.misc.warning("Failed to parse OpenGL version (parsing int): " + "{}".format(version)) + return cls(vendor=vendor, version_str=version) + + return cls(vendor=vendor, version_str=version, + version=parsed_version, vendor_specific=vendor_specific) + + +@functools.lru_cache(maxsize=1) +def opengl_info() -> typing.Optional[OpenGLInfo]: # pragma: no cover """Get the OpenGL vendor used. This returns a string such as 'nouveau' or @@ -496,10 +545,14 @@ def opengl_vendor() -> typing.Optional[str]: # pragma: no cover """ assert QApplication.instance() - override = os.environ.get('QUTE_FAKE_OPENGL_VENDOR') + # Some setups can segfault in here if we don't do this. + utils.libgl_workaround() + + override = os.environ.get('QUTE_FAKE_OPENGL') if override is not None: log.init.debug("Using override {}".format(override)) - return override + vendor, version = override.split(', ', maxsplit=1) + return OpenGLInfo.parse(vendor=vendor, version=version) old_context = typing.cast(typing.Optional[QOpenGLContext], QOpenGLContext.currentContext()) @@ -522,7 +575,7 @@ def opengl_vendor() -> typing.Optional[str]: # pragma: no cover try: if ctx.isOpenGLES(): # Can't use versionFunctions there - return None + return OpenGLInfo(gles=True) vp = QOpenGLVersionProfile() vp.setVersion(2, 0) @@ -537,7 +590,10 @@ def opengl_vendor() -> typing.Optional[str]: # pragma: no cover log.init.debug("Getting version functions failed!") return None - return vf.glGetString(vf.GL_VENDOR) + vendor = vf.glGetString(vf.GL_VENDOR) + version = vf.glGetString(vf.GL_VERSION) + + return OpenGLInfo.parse(vendor=vendor, version=version) finally: ctx.doneCurrent() if old_context and old_surface: diff --git a/tests/conftest.py b/tests/conftest.py index c6b6c2efc..e698bde74 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,8 +25,6 @@ import os import sys import warnings import pathlib -import ctypes -import ctypes.util import pytest import hypothesis @@ -258,9 +256,7 @@ def set_backend(monkeypatch, request): @pytest.fixture(autouse=True, scope='session') def apply_libgl_workaround(): """Make sure we load libGL early so QtWebEngine tests run properly.""" - libgl = ctypes.util.find_library("GL") - if libgl is not None: - ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL) + utils.libgl_workaround() @pytest.fixture(autouse=True) diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index 0a3c5e4aa..903585b62 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -35,6 +35,8 @@ import datetime import attr import pkg_resources import pytest +import hypothesis +import hypothesis.strategies import qutebrowser from qutebrowser.config import config @@ -956,11 +958,15 @@ def test_version_output(params, stubs, monkeypatch, config_stub): 'config.instance.yaml_loaded': params.autoconfig_loaded, } + version.opengl_info.cache_clear() + monkeypatch.setenv('QUTE_FAKE_OPENGL', 'VENDOR, 1.0 VERSION') + substitutions = { 'git_commit': '\nGit commit: GIT COMMIT' if params.git_commit else '', 'style': '\nStyle: STYLE' if params.qapp else '', 'platform_plugin': ('\nPlatform plugin: PLATFORM' if params.qapp else ''), + 'opengl': '\nOpenGL: VENDOR, 1.0 VERSION' if params.qapp else '', 'qt': 'QT VERSION', 'frozen': str(params.frozen), 'import_path': import_path, @@ -1026,7 +1032,7 @@ def test_version_output(params, stubs, monkeypatch, config_stub): pdf.js: PDFJS VERSION sqlite: SQLITE VERSION QtNetwork SSL: {ssl} - {style}{platform_plugin} + {style}{platform_plugin}{opengl} Platform: PLATFORM, ARCHITECTURE{linuxdist} Frozen: {frozen} Imported from {import_path} @@ -1045,10 +1051,76 @@ def test_version_output(params, stubs, monkeypatch, config_stub): assert version.version() == expected -def test_opengl_vendor(qapp): - """Simply call version.opengl_vendor() and see if it doesn't crash.""" - pytest.importorskip("PyQt5.QtOpenGL") - return version.opengl_vendor() +class TestOpenGLInfo: + + @pytest.fixture(autouse=True) + def cache_clear(self): + """Clear the lru_cache between tests.""" + version.opengl_info.cache_clear() + + def test_func(self, qapp): + """Simply call version.opengl_info() and see if it doesn't crash.""" + pytest.importorskip("PyQt5.QtOpenGL") + version.opengl_info() + + def test_func_fake(self, qapp, monkeypatch): + monkeypatch.setenv('QUTE_FAKE_OPENGL', 'Outtel Inc., 3.0 Messiah 20.0') + info = version.opengl_info() + assert info.vendor == 'Outtel Inc.' + assert info.version_str == '3.0 Messiah 20.0' + assert info.version == (3, 0) + assert info.vendor_specific == 'Messiah 20.0' + + @pytest.mark.parametrize('version_str, reason', [ + ('blah', 'missing space'), + ('2,x blah', 'parsing int'), + ]) + def test_parse_invalid(self, caplog, version_str, reason): + with caplog.at_level(logging.WARNING): + info = version.OpenGLInfo.parse(vendor="vendor", + version=version_str) + + assert info.version is None + assert info.vendor_specific is None + assert info.vendor == 'vendor' + assert info.version_str == version_str + + msg = "Failed to parse OpenGL version ({}): {}".format( + reason, version_str) + assert caplog.messages == [msg] + + @hypothesis.given(vendor=hypothesis.strategies.text(), + version_str=hypothesis.strategies.text()) + def test_parse_hypothesis(self, caplog, vendor, version_str): + with caplog.at_level(logging.WARNING): + info = version.OpenGLInfo.parse(vendor=vendor, version=version_str) + + assert info.vendor == vendor + assert info.version_str == version_str + assert vendor in str(info) + assert version_str in str(info) + + if info.version is not None: + reconstructed = ' '.join(['.'.join(str(part) + for part in info.version), + info.vendor_specific]) + assert reconstructed == info.version_str + + @pytest.mark.parametrize('version_str, expected', [ + ("2.1 INTEL-10.36.26", (2, 1)), + ("4.6 (Compatibility Profile) Mesa 20.0.7", (4, 6)), + ("3.0 Mesa 20.0.7", (3, 0)), + ("3.0 Mesa 20.0.6", (3, 0)), + # Not from the wild, but can happen according to standards + ("3.0.2 Mesa 20.0.6", (3, 0, 2)), + ]) + def test_version(self, version_str, expected): + info = version.OpenGLInfo.parse(vendor='vendor', version=version_str) + assert info.version == expected + + def test_str_gles(self): + info = version.OpenGLInfo(gles=True) + assert str(info) == 'OpenGL ES' @pytest.fixture |