Skip to content

Commit

Permalink
Merge pull request #2375 from Holzhaus/qsscheck
Browse files Browse the repository at this point in the history
Add qsscheck to CI pipeline and improve .travis.yml config
  • Loading branch information
Holzhaus authored Dec 9, 2019
2 parents 80ec75a + 7ba04af commit 1456b8b
Show file tree
Hide file tree
Showing 2 changed files with 237 additions and 42 deletions.
98 changes: 56 additions & 42 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,65 @@
language: c++

matrix:

# Build flags common to OS X and Linux.
# Parallel builds are important for avoiding OSX build timeouts.
# We turn off verbose output to avoid going over the 4MB output limit.
# TODO(2019-07-21): Add "ffmpeg=1" if FFmpeg 4.x becomes available in Ubuntu
env:
global:
- COMMON_FLAGS="-j4 test=1 mad=1 faad=1 opus=1 modplug=1 wv=1 hss1394=0 virtualize=0 debug_assertions_fatal=1 verbose=0"

jobs:
include:
- os: linux
- name: qsscheck
os: linux
dist: xenial
before_install:
- pip3 install tinycss
script:
- ./scripts/qsscheck.py .
addons:
apt:
packages:
- python3
- python3-pip
- python3-pyqt5
- python3-setuptools
- python3-wheel

- name: Ubuntu/gcc build
os: linux
dist: xenial
sudo: required
compiler: gcc
- os: osx
# Ubuntu Xenial build prerequisites
env: EXTRA_FLAGS="localecompare=1"
install:
- scons $COMMON_FLAGS $EXTRA_FLAGS
script:
# NOTE(sblaisot): 2018-01-02 removing gdb wrapper on linux due to a bug in
# return code in order to avoid having a successful build when a test fail.
# https://bugs.launchpad.net/mixxx/+bug/1699689
- ./mixxx-test

- name: OSX/clang build
os: osx
compiler: clang
# Workaround for bug in libopus's opus.h including <opus_multistream.h>
# instead of <opus/opus_multistream.h>.
# Virtual X (Xvfb) is needed for analyzer waveform tests
env: >-
CFLAGS="-isystem /usr/local/include/opus"
CXXFLAGS="-isystem /usr/local/include/opus"
DISPLAY=:99.0
before_install:
- export QTDIR="$(find /usr/local/Cellar/qt -d 1 | tail -n 1)"
- echo "QTDIR=$QTDIR"
install:
- scons $COMMON_FLAGS $EXTRA_FLAGS
script:
# lldb doesn't provide an easy way to exit 1 on error:
# https://bugs.llvm.org/show_bug.cgi?id=27326
- lldb ./mixxx-test --batch -o run -o quit -k 'thread backtrace all' -k "script import os; os._exit(1)"

git:
depth: 1
Expand Down Expand Up @@ -82,44 +134,6 @@ addons:
- taglib
- wavpack

install:
# Build flags common to OS X and Linux.
# Parallel builds are important for avoiding OSX build timeouts.
# We turn off verbose output to avoid going over the 4MB output limit.
# TODO(2019-07-21): Add "ffmpeg=1" if FFmpeg 4.x becomes available in Ubuntu
- export COMMON_FLAGS="-j4 test=1 mad=1 faad=1 opus=1 modplug=1 wv=1 hss1394=0 virtualize=0 debug_assertions_fatal=1 verbose=0"

# Ubuntu Xenial build prerequisites
- if [ "$TRAVIS_OS_NAME" = "linux" ]; then export EXTRA_FLAGS="localecompare=1"; fi

# Define QTDIR.
- if [ "$TRAVIS_OS_NAME" = "osx" ]; then export QTDIR=$(find /usr/local/Cellar/qt -d 1 | tail -n 1); fi

# Workaround for bug in libopus's opus.h including <opus_multistream.h>
# instead of <opus/opus_multistream.h>.
- if [ "$TRAVIS_OS_NAME" = "osx" ]; then export CXXFLAGS="-isystem /usr/local/include/opus"; fi
- if [ "$TRAVIS_OS_NAME" = "osx" ]; then export CFLAGS="-isystem /usr/local/include/opus"; fi

# NOTE(rryan): 2016-11-15 we are experiencing Travis timeouts for the OSX
# build. Turning off optimizations to see if that speeds up compile times.
# TODO(rryan): localecompare doesn't work on Travis with qt5 for some reason.
# TODO(2019-07-21): Move "ffmpeg=1" into COMMON_FLAGS if FFmpeg 4.x becomes available in Ubuntu
- if [ "$TRAVIS_OS_NAME" = "osx" ]; then export EXTRA_FLAGS="ffmpeg=1 optimize=none asan=0 localecompare=0"; fi

- scons $COMMON_FLAGS $EXTRA_FLAGS

before_script:
# Virtual X (Xvfb) is needed for analyzer waveform tests
- if [ "$TRAVIS_OS_NAME" = "linux" ]; then export DISPLAY=:99.0; fi

script:
# NOTE(sblaisot): 2018-01-02 removing gdb wrapper on linux due to a bug in
# return code in order to avoid having a successful build when a test fail.
# https://bugs.launchpad.net/mixxx/+bug/1699689
- if [ "$TRAVIS_OS_NAME" = "linux" ]; then ./mixxx-test; fi
# lldb doesn't provide an easy way to exit 1 on error:
# https://bugs.llvm.org/show_bug.cgi?id=27326
- if [ "$TRAVIS_OS_NAME" = "osx" ]; then lldb ./mixxx-test --batch -o run -o quit -k 'thread backtrace all' -k "script import os; os._exit(1)"; fi

notifications:
webhooks:
Expand Down
181 changes: 181 additions & 0 deletions scripts/qsscheck.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import fnmatch
import os
import os.path
import re
import sys
import tinycss.css21
import PyQt5.QtWidgets


RE_CPP_CLASSNAME = re.compile(r'^\s*class\s+([\w_]+)')
RE_CPP_OBJNAME = re.compile(r'setObjectName\(.*"([^"]+)"')
RE_UI_OBJNAME = re.compile(r'<widget[^>]+name="([^"]+)"')
RE_XML_OBJNAME = re.compile(r'<ObjectName>(.*)</ObjectName>')
RE_XML_OBJNAME_SETVAR = re.compile(
r'<SetVariable\s+name="ObjectName">(.*)</SetVariable>')
RE_CLASSNAME = re.compile(r'^[A-Z]\w+$')
RE_OBJNAME_VARTAG = re.compile(r'<.*>')


def get_skins(path):
"""Yields (skin_name, skin_path) tuples for each skin directory in path."""
for entry in os.scandir(path):
if entry.is_dir():
yield entry.name, os.path.join(path, entry.name)


def get_global_names(mixxx_path):
"""Returns 2 sets with all class and object names in the Mixx codebase."""
classnames = set()
objectnames = set()
for root, dirs, fnames in os.walk(os.path.join(mixxx_path, 'src')):
for fname in fnames:
ext = os.path.splitext(fname)[1]
if ext in ('.h', '.cpp'):
fpath = os.path.join(root, fname)
with open(fpath, mode='r') as f:
for line in f:
classnames.update(set(RE_CPP_CLASSNAME.findall(line)))
objectnames.update(set(RE_CPP_OBJNAME.findall(line)))
elif ext == '.ui':
fpath = os.path.join(root, fname)
with open(fpath, mode='r') as f:
objectnames.update(set(RE_UI_OBJNAME.findall(f.read())))
return classnames, objectnames


def get_skin_objectnames(skin_path):
"""
Yields all object names in the skin_path.
Note the names may contain one or more <Variable name="x"> tags, so it's
not enough to check if a name CSS object name is in this list using "in".
"""
for root, dirs, fnames in os.walk(skin_path):
for fname in fnames:
if os.path.splitext(fname)[1] != '.xml':
continue

fpath = os.path.join(root, fname)
with open(fpath, mode='r') as f:
for line in f:
yield from RE_XML_OBJNAME.findall(line)
yield from RE_XML_OBJNAME_SETVAR.findall(line)


def get_skin_stylesheets(skin_path):
"""Yields (qss_path, stylesheet) tuples for each qss file in skin_path)."""
cssparser = tinycss.css21.CSS21Parser()
for filename in os.listdir(skin_path):
if os.path.splitext(filename)[1] != '.qss':
continue
qss_path = os.path.join(skin_path, filename)
stylesheet = cssparser.parse_stylesheet_file(qss_path)
yield qss_path, stylesheet


def check_stylesheet(stylesheet, classnames, objectnames, objectnames_fuzzy):
"""Yields (token, problem) tuples for each problem found in stylesheet."""
for rule in stylesheet.rules:
if not isinstance(rule, tinycss.css21.RuleSet):
continue
for token in rule.selector:
if token.type == 'IDENT':
if not RE_CLASSNAME.match(token.value):
continue
if token.value in classnames:
continue
if token.value in dir(PyQt5.QtWidgets):
continue
yield (token, 'Unknown widget class "%s"' % token.value)

elif token.type == 'HASH':
value = token.value[1:]
if value in objectnames:
continue

if any(fnmatch.fnmatchcase(value, objname)
for objname in objectnames_fuzzy):
continue

yield (token, 'Unknown object name "%s"' % token.value)


def check_skins(mixxx_path, skins, ignore_patterns=()):
"""
Yields error messages for skins using class/object names from mixxx_path.
By providing a list of ignore_patterns, you can ignore certain class or
object names (e.g. #Test, #*Debug).
"""
classnames, objectnames = get_global_names(mixxx_path)
for skin_name, skin_path in sorted(skins):
# If the skin objectname is something like 'Deck<Variable name="i">',
# then replace it with 'Deck*' and use glob-like matching
skin_objectnames = objectnames.copy()
skin_objectnames_fuzzy = set()
for objname in get_skin_objectnames(skin_path):
new_objname = RE_OBJNAME_VARTAG.sub('*', objname)
if '*' in new_objname:
skin_objectnames_fuzzy.add(new_objname)
else:
skin_objectnames.add(new_objname)

for qss_path, stylesheet in get_skin_stylesheets(skin_path):
for error in stylesheet.errors:
yield '%s:%d:%d: %s - %s' % (
qss_path, error.line, error.column,
error.__class__.__name__, error.reason,
)
for token, message in check_stylesheet(
stylesheet, classnames,
skin_objectnames, skin_objectnames_fuzzy):
if any(fnmatch.fnmatchcase(token.value, pattern)
for pattern in ignore_patterns):
continue
yield '%s:%d:%d: %s' % (
qss_path, token.line, token.column, message,
)


def main(argv=None):
"""Main method for handling command line arguments."""
parser = argparse.ArgumentParser('qsscheck', description='Check Mixxx QSS '
'stylesheets for non-existing '
'object/class names')
parser.add_argument('-p', '--extra-skins-path',
help='Additonal skin path, to check (.e.g. '
'"~/.mixxx/skins")')
parser.add_argument('-s', '--skin', help='Only check skin with this name')
parser.add_argument('-i', '--ignore', default='',
help='Glob pattern of class/object names to ignore '
'(e.g. "#Test*"), separated by commas')
parser.add_argument('mixxx_path', help='Path of Mixxx sources/git repo')
args = parser.parse_args(argv)

mixxx_path = args.mixxx_path

skins_path = os.path.join(mixxx_path, 'res', 'skins')
skins = set(get_skins(skins_path))
if args.extra_skins_path:
skins.update(set(get_skins(args.extra_skins_path)))

if args.skin:
skins = set((name, path) for name, path in skins if name == args.skin)

if not skins:
print('No skins to check')
return 1

status = 0
for message in check_skins(mixxx_path, skins, args.ignore.split(',')):
print(message)
status = 2
return status


if __name__ == '__main__':
sys.exit(main())

0 comments on commit 1456b8b

Please sign in to comment.