Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2c00901
scripts: Add qsscheck.py script
Holzhaus Nov 28, 2019
151fe9e
.travis.yml: Run qsscheck.py on Linux CI builds
Holzhaus Nov 28, 2019
66c5ab4
.travis.yml: Cleanup travis config and run linter as separate job
Holzhaus Nov 29, 2019
dceea54
.travis.yml: Add job names
Holzhaus Nov 29, 2019
44f970e
.travis.yml: Fix setting global env variables
Holzhaus Nov 29, 2019
4acbcfb
.travis.yml: Remove unused before_script for osx builds
Holzhaus Nov 29, 2019
deb4c23
.travis.yml: Print QTDIR on osx builds
Holzhaus Nov 29, 2019
65724d5
.travis.yml: Use python 3.7 by default on OSX
Holzhaus Nov 29, 2019
91c7064
.travis.yml: Replace "matrix" alias with more descriptive "jobs"
Holzhaus Nov 29, 2019
4a075fb
Revert ".travis.yml: Use python 3.7 by default on OSX"
Holzhaus Nov 29, 2019
35e4801
.travis.yml: Only install python3 on qsscheck
Holzhaus Nov 29, 2019
779fd9f
scripts/qsscheck: Pre-compile regexes
Holzhaus Dec 1, 2019
d85fadd
scripts/qsscheck: Reduce indention depth using continue statement
Holzhaus Dec 1, 2019
6360f9f
scripts/qsscheck: Only run glob-matching for actual glob objectnames
Holzhaus Dec 1, 2019
af9f350
scripts/qsscheck: Improve qscheck script and add -p parameter
Holzhaus Dec 1, 2019
b723e5f
scripts/qsscheck: Move core logic from main() into separate function
Holzhaus Dec 1, 2019
8e29cf2
scripts/qsscheck: Fix line exceeding maximum line lengths
Holzhaus Dec 1, 2019
f60bdd8
scripts/qsscheck: Add docstrings for all functions
Holzhaus Dec 1, 2019
9bd7d84
Merge branch 'master' of https://github.com/mixxxdj/mixxx into qsscheck
Holzhaus Dec 8, 2019
7ba04af
Merge branch 'master' of https://github.com/mixxxdj/mixxx into qsscheck
Holzhaus Dec 8, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(see below comment -- here's another place where we can save a level of indentation)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How? We have an elseif case, too.

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())