Improve test infrastructure.

- Reanimate vmtests.
- Use waf to run tests.
- Run clang-tidy of C++ sources.
- Ensure noisicaä still runs on Ubuntu 16.04.
- Upgrade mypy to 0.720 and fix new issues.
pyside
Ben Niemann 4 years ago
parent 55c999aed5
commit 53ccc97183

@ -6,7 +6,7 @@
((nil . (
; Projetile
(projectile-project-test-cmd . "bin/runtests")
(projectile-project-test-cmd . "./waf test")
(pyvenv-workon . "noisicaa")

@ -1,2 +1,3 @@
-/build
-/venv
-/vmtests

@ -0,0 +1,2 @@
from typing import Any
def __getattr__(arrr: str) -> Any: ...

@ -0,0 +1,2 @@
from typing import Any
def __getattr__(arrr: str) -> Any: ...

@ -0,0 +1,2 @@
from typing import Any
def __getattr__(arrr: str) -> Any: ...

@ -0,0 +1,2 @@
from typing import Any
def __getattr__(arrr: str) -> Any: ...

@ -0,0 +1,2 @@
from typing import Any
def __getattr__(arrr: str) -> Any: ...

@ -0,0 +1,2 @@
from typing import Any
def __getattr__(arrr: str) -> Any: ...

@ -1,36 +0,0 @@
#!/bin/bash
# @begin:license
#
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# 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, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# @end:license
BASEDIR=$(readlink -f "$(dirname "$0")")
(
set -e
$BASEDIR/runpylint -E noisicaa
$BASEDIR/runtests
)
if [ $? -gt 0 ]; then
echo
echo "****** THERE WERE ERRORS ******"
echo
exit 1
fi

@ -1,7 +1,7 @@
Custom `waf` tools used for building noisicaä.
Running pylint on these files (these files are used to build, they are not built themselves, so
`runtests` does not know about them):
`./waf test` does not know about them):
```bash
PYTHONPATH=$(ls -d .waf*) bin/runpylint build_utils.waf

@ -0,0 +1,126 @@
# -*- mode: python -*-
# @begin:license
#
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# 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, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# @end:license
import os
import os.path
import subprocess
import sys
from waflib.Configure import conf
from waflib.Task import Task
from waflib import Utils
def configure(ctx):
if ctx.env.ENABLE_TEST:
ctx.find_program('clang-tidy-8', var='CLANG_TIDY', mandatory=False)
class run_clang_tidy(Task):
always_run = True
def __str__(self):
return self.inputs[0].relpath()
def keyword(self):
return 'Lint (clang-tidy)'
@property
def mod_name(self):
mod_path = self.inputs[0].relpath()
assert mod_path.endswith('.cpp')
return '.'.join(os.path.splitext(mod_path)[0].split(os.sep))
@property
def test_id(self):
return self.mod_name + ':clang-tidy'
def run(self):
ctx = self.generator.bld
success = True
try:
argv = [
ctx.env.CLANG_TIDY[0],
'-quiet',
self.inputs[0].relpath(),
'--',
'-Wall',
'-I.', '-Ibuild',
]
argv += ['-I%s' % p for p in ctx.env.INCLUDES_LILV]
argv += ['-I%s' % p for p in ctx.env.INCLUDES_SUIL]
argv += ['-I%s' % p for p in ctx.env.INCLUDES_GTK2]
argv += ['-I%s' % p for p in ctx.env.INCLUDES]
env = dict(os.environ)
kw = {
'cwd': ctx.top_dir,
'env': env,
'stdout': subprocess.PIPE,
'stderr': subprocess.PIPE,
}
ctx.log_command(argv, kw)
_, out, _ = Utils.run_process(argv, kw)
out = out.strip()
if out:
success = False
out_path = os.path.join(ctx.TEST_RESULTS_PATH, self.mod_name, 'clang-tidy.log')
os.makedirs(os.path.dirname(out_path), exist_ok=True)
with open(out_path, 'wb') as fp:
fp.write(out)
if out and ctx.options.fail_fast:
sys.stderr.write(out.decode('utf-8'))
sys.stderr.write('\n')
raise RuntimeError("clang-tidy for %s failed." % self.mod_name)
except Exception:
success = False
raise
finally:
ctx.record_test_state(self.test_id, success)
@conf
def cpp_module(ctx, source, **kwargs):
assert source.endswith('.cpp')
source_node = ctx.path.make_node(source)
if (ctx.cmd == 'test'
and ctx.env.CLANG_TIDY
and ctx.should_run_test(source_node)
and {'all', 'lint', 'clang-tidy'} & ctx.TEST_TAGS):
with ctx.group(ctx.GRP_RUN_TESTS):
task = run_clang_tidy(env=ctx.env)
task.set_inputs(source_node)
if not ctx.options.only_failed or not ctx.get_test_state(task.test_id):
ctx.add_to_group(task)
return source_node

@ -21,9 +21,12 @@
# @end:license
import os.path
import subprocess
import sys
from waflib.Configure import conf
from waflib.Task import Task
from waflib import Utils
def configure(ctx):
@ -41,24 +44,41 @@ class compile_csound(Task):
ctx = self.generator.bld
cwd = ctx.srcnode
env = {
'LD_LIBRARY_PATH': os.path.join(ctx.env.VIRTUAL_ENV, 'lib'),
}
cmd = [
ctx.env.CSOUND[0],
'-o' + self.outputs[0].path_from(cwd),
self.inputs[0].path_from(cwd),
]
return self.exec_command(cmd, cwd=cwd, env=env)
kw = {
'cwd': cwd.abspath(),
'stdout': subprocess.PIPE,
'stderr': subprocess.STDOUT,
}
ctx.log_command(cmd, kw)
rc, out, _ = Utils.run_process(cmd, kw)
if rc:
sys.stderr.write(out.decode('utf-8'))
return rc
@conf
def rendered_csound(ctx, source):
def rendered_csound(ctx, source, install=None, install_to=None, chmod=0o644):
assert source.endswith('.csnd')
wav_path = os.path.splitext(source)[0] + '.wav'
target = ctx.path.get_bld().make_node(wav_path)
task = compile_csound(env=ctx.env)
task.set_inputs(ctx.path.find_resource(source))
wav_path = os.path.splitext(source)[0] + '.wav'
task.set_outputs(ctx.path.get_bld().make_node(wav_path))
task.set_outputs(target)
ctx.add_to_group(task)
if install is None:
install = ctx.in_group(ctx.GRP_BUILD_MAIN)
if install:
if install_to is None:
install_to = os.path.join(
ctx.env.DATADIR, target.parent.path_from(ctx.bldnode.make_node('data')))
ctx.install_files(install_to, target, chmod=chmod)

@ -90,7 +90,7 @@ def cy_module(ctx, source, use=None):
install_path=None,
)
if ctx.get_group_name(ctx.current_group) == 'noisicaa':
if ctx.in_group(ctx.GRP_BUILD_MAIN):
ctx.install_files(os.path.join(ctx.env.LIBDIR, mod.parent.relpath()), mod)
if pxd.exists():
@ -99,14 +99,15 @@ def cy_module(ctx, source, use=None):
if pyi.exists():
ctx.static_file(pyi, install=False)
return mod
@conf
def cy_test(ctx, source, use=None):
def cy_test(ctx, source, use=None, **kwargs):
if not ctx.env.ENABLE_TEST:
return
old_grp = ctx.current_group
ctx.set_group('tests')
try:
ctx.cy_module(source, use=use)
finally:
ctx.set_group(old_grp)
with ctx.group(ctx.GRP_BUILD_TESTS):
target = ctx.cy_module(source, use=use)
ctx.add_py_test_runner(target, **kwargs)

@ -49,14 +49,13 @@ def faust_dsp(ctx, cls_name, source='processor.dsp'):
],
cls_name=cls_name)
if ctx.get_group_name(ctx.current_group) == 'noisicaa':
if ctx.in_group(ctx.GRP_BUILD_MAIN):
ctx.install_files(os.path.join(ctx.env.LIBDIR, json.parent.relpath()), json)
ctx.shlib(
target='noisicaa-builtin_nodes-%s-processor' % cls_name.lower(),
source='processor.cpp',
use=[
'NOISELIB',
'noisicaa-audioproc-public',
'noisicaa-host_system',
],

@ -27,7 +27,6 @@ import os.path
import pathlib
import shutil
import subprocess
import textwrap
import packaging.markers
import packaging.requirements
@ -64,7 +63,7 @@ def install_runtime_pip_packages(ctx):
stdout=subprocess.PIPE, check=True)
installed_packages = {
packaging.utils.canonicalize_name(p['name']): (p['name'], p['version'])
for p in json.loads(p.stdout)}
for p in json.loads(p.stdout.decode('utf-8'))}
required_packages = set()
@ -108,7 +107,7 @@ def install_runtime_pip_packages(ctx):
# File is not under site-packages.
continue
dest_path = os.path.join(ctx.env.LIBDIR, rel_path)
dest_path = os.path.join(ctx.env.LIBDIR, str(rel_path))
if not ctx.progress_bar:
Logs.info(
@ -118,5 +117,5 @@ def install_runtime_pip_packages(ctx):
if not os.path.isdir(os.path.dirname(dest_path)):
os.makedirs(os.path.dirname(dest_path))
shutil.copyfile(src_path, dest_path)
shutil.copystat(src_path, dest_path)
shutil.copyfile(str(src_path), dest_path)
shutil.copystat(str(src_path), dest_path)

@ -42,7 +42,7 @@ class build_model(Task):
'--template', self.inputs[1].abspath(),
os.path.relpath(self.inputs[0].abspath(), ctx.top_dir),
]
return self.exec_command(cmd, cwd=ctx.top_dir, env={'PYTHONPATH': ctx.out_dir})
return self.exec_command(cmd, cwd=ctx.top_dir)
@conf
@ -58,5 +58,5 @@ def model_description(
os.path.join(os.path.dirname(output), 'model.proto')))
ctx.add_to_group(task)
if ctx.get_group_name(ctx.current_group) == 'noisicaa':
if ctx.in_group(ctx.GRP_BUILD_MAIN):
ctx.install_files(os.path.join(ctx.env.LIBDIR, model_node.parent.relpath()), model_node)

@ -86,7 +86,7 @@ def py_proto(ctx, source):
task.set_outputs(ctx.path.get_bld().make_node(pyi_path))
ctx.add_to_group(task)
if ctx.get_group_name(ctx.current_group) == 'noisicaa':
if ctx.in_group(ctx.GRP_BUILD_MAIN):
ctx.install_files(os.path.join(ctx.env.LIBDIR, pb2_node.parent.relpath()), pb2_node)
ctx.install_files(os.path.join(ctx.env.LIBDIR, pb2c_node.parent.relpath()), pb2c_node)
@ -132,3 +132,5 @@ def cpp_proto(ctx, source):
task.set_outputs(ctx.path.get_bld().make_node(
os.path.splitext(source)[0] + '.pb.h'))
ctx.add_to_group(task)
return os.path.splitext(source)[0] + '.pb.cc'

@ -21,11 +21,17 @@
# @end:license
import importlib.util
import os
import os.path
import py_compile
import shutil
import subprocess
import sys
import threading
from waflib.Configure import conf
from waflib.Task import Task
from waflib import Utils
def copy_py_module(task):
@ -38,9 +44,179 @@ def copy_py_module(task):
task.outputs[0].abspath(), task.outputs[1].abspath(), doraise=True, optimize=0)
# Multiple concurrent mypy processes cannot share the same cache directory. So we track a set of
# directories and allocate an unused directory for each running process.
mypy_cache_lock = threading.Lock()
mypy_caches = []
mypy_next_cache = 0
class run_mypy(Task):
always_run = True
def __init__(self, *, env, strict):
super().__init__(env=env)
self.__strict = strict
def __str__(self):
return self.inputs[0].relpath()
def keyword(self):
return 'Lint (mypy)'
@property
def mod_name(self):
mod_path = self.inputs[0].relpath()
assert mod_path.endswith('.py') or mod_path.endswith('.so')
return '.'.join(os.path.splitext(mod_path)[0].split(os.sep))
@property
def test_id(self):
return self.mod_name + ':mypy'
def run(self):
ctx = self.generator.bld
success = True
try:
ini_path = os.path.join(ctx.top_dir, 'noisidev', 'mypy.ini')
with mypy_cache_lock:
if not mypy_caches:
global mypy_next_cache # pylint: disable=global-statement
cache_num = mypy_next_cache
mypy_next_cache += 1
else:
cache_num = mypy_caches.pop(-1)
try:
argv = [
os.path.join(ctx.env.VIRTUAL_ENV, 'bin', 'mypy'),
'--config-file', ini_path,
'--cache-dir=%s' % os.path.join(ctx.out_dir, 'mypy-cache.%d' % cache_num),
'--show-traceback',
'-m', self.mod_name,
]
if self.__strict:
argv.append('--disallow-untyped-defs')
env = dict(os.environ)
env['MYPYPATH'] = os.path.join(ctx.top_dir, '3rdparty', 'typeshed')
kw = {
'cwd': ctx.out_dir,
'env': env,
'stdout': subprocess.PIPE,
'stderr': subprocess.PIPE,
}
ctx.log_command(argv, kw)
_, out, err = Utils.run_process(argv, kw)
out = out.strip()
if out:
success = False
finally:
with mypy_cache_lock:
mypy_caches.append(cache_num)
if err:
sys.stderr.write(err.decode('utf-8'))
raise RuntimeError("mypy is unhappy")
out_path = os.path.join(ctx.TEST_RESULTS_PATH, self.mod_name, 'mypy.log')
os.makedirs(os.path.dirname(out_path), exist_ok=True)
with open(out_path, 'wb') as fp:
fp.write(out)
if out and ctx.options.fail_fast:
sys.stderr.write(out.decode('utf-8'))
sys.stderr.write('\n')
raise RuntimeError("mypy for %s failed." % self.mod_name)
except Exception:
success = False
raise
finally:
ctx.record_test_state(self.test_id, success)
class run_pylint(Task):
always_run = True
def __str__(self):
return self.inputs[0].relpath()
def keyword(self):
return 'Lint (pylint)'
@property
def mod_name(self):
mod_path = self.inputs[0].relpath()
assert mod_path.endswith('.py') or mod_path.endswith('.so')
return '.'.join(os.path.splitext(mod_path)[0].split(os.sep))
@property
def test_id(self):
return self.mod_name + ':pylint'
def run(self):
ctx = self.generator.bld
success = True
try:
argv = [
os.path.join(ctx.env.VIRTUAL_ENV, 'bin', 'pylint'),
'--rcfile=%s' % os.path.join(ctx.top_dir, 'bin', 'pylintrc'),
'--output-format=parseable',
'--score=no',
'--exit-zero',
self.mod_name,
]
kw = {
'cwd': ctx.out_dir,
'stdout': subprocess.PIPE,
'stderr': subprocess.PIPE,
}
ctx.log_command(argv, kw)
rc, out, err = Utils.run_process(argv, kw)
out = out.strip()
if out:
success = False
if rc != 0:
sys.stderr.write(err.decode('utf-8'))
raise RuntimeError("pylint is unhappy")
out_path = os.path.join(ctx.TEST_RESULTS_PATH, self.mod_name, 'pylint.log')
os.makedirs(os.path.dirname(out_path), exist_ok=True)
with open(out_path, 'wb') as fp:
fp.write(out)
if out and ctx.options.fail_fast:
sys.stderr.write(out.decode('utf-8'))
sys.stderr.write('\n')
raise RuntimeError("pylint for %s failed." % self.mod_name)
except Exception:
success = False
raise
finally:
ctx.record_test_state(self.test_id, success)
@conf
def py_module(ctx, source):
def py_module(ctx, source, mypy='strict', pylint='enabled'):
assert source.endswith('.py')
assert mypy in ('strict', 'loose', 'disabled')
assert pylint in ('enabled', 'disabled')
source_node = ctx.path.make_node(source)
target_node = ctx.path.get_bld().make_node(source)
@ -54,21 +230,128 @@ def py_module(ctx, source):
compiled_node,
])
if ctx.get_group_name(ctx.current_group) == 'noisicaa':
if ctx.in_group(ctx.GRP_BUILD_MAIN):
ctx.install_files(
os.path.join(ctx.env.LIBDIR, target_node.parent.relpath()), target_node)
ctx.install_files(
os.path.join(ctx.env.LIBDIR, compiled_node.parent.relpath()), compiled_node)
if source == '__init__.py':
mypy = 'disabled'
if ctx.in_group(ctx.GRP_BUILD_TOOLS):
mypy = 'disabled'
pylint = 'disabled'
if (ctx.cmd == 'test'
and ctx.should_run_test(target_node)
and {'all', 'lint', 'mypy'} & ctx.TEST_TAGS
and mypy != 'disabled'):
with ctx.group(ctx.GRP_RUN_TESTS):
task = run_mypy(env=ctx.env, strict=(mypy == 'strict'))
task.set_inputs(target_node)
if not ctx.options.only_failed or not ctx.get_test_state(task.test_id):
ctx.add_to_group(task)
if (ctx.cmd == 'test'
and ctx.should_run_test(target_node)
and {'all', 'lint', 'pylint'} & ctx.TEST_TAGS
and pylint != 'disabled'):
with ctx.group(ctx.GRP_RUN_TESTS):
task = run_pylint(env=ctx.env)
task.set_inputs(target_node)
if not ctx.options.only_failed or not ctx.get_test_state(task.test_id):
ctx.add_to_group(task)
return target_node
class run_py_test(Task):
always_run = True
def __init__(self, *, env, timeout=None):
super().__init__(env=env)
self.__timeout = timeout or 60
assert self.__timeout > 0
def __str__(self):
return self.inputs[0].relpath()
def keyword(self):
return 'Testing'
@property
def mod_name(self):
mod_path = self.inputs[0].relpath()
assert mod_path.endswith('.py') or mod_path.endswith('.so')
return '.'.join(os.path.splitext(mod_path)[0].split(os.sep))
@property
def test_id(self):
return self.mod_name + ':unit'
def run(self):
ctx = self.generator.bld
success = True
try:
results_path = os.path.join(ctx.TEST_RESULTS_PATH, self.mod_name)
cmd = [
ctx.env.PYTHON[0],
'-m', 'noisidev.test_runner',
'--set-rc=false',
'--store-result=%s' % results_path,
'--coverage=%s' % ('true' if ctx.options.coverage else 'false'),
self.mod_name,
]
rc = self.exec_command(
cmd,
cwd=ctx.out_dir,
timeout=self.__timeout)
if rc != 0:
raise RuntimeError("test_runner failed.")
if not os.path.isfile(os.path.join(results_path, 'results.xml')):
raise RuntimeError("Missing results.xml.")
if rc != 0 and ctx.options.fail_fast:
if os.path.isfile(os.path.join(results_path, 'test.log')):
with open(os.path.join(results_path, 'test.log'), 'r') as fp:
sys.stderr.write(fp.read())
raise RuntimeError("Tests for %s failed." % self.mod_name)
except Exception:
success = False
raise
finally:
ctx.record_test_state(self.test_id, success)
@conf
def add_py_test_runner(ctx, target, tags=None, timeout=None):
if tags is None:
tags = {'unit'}
if (ctx.cmd == 'test'
and ('all' in ctx.TEST_TAGS or tags & ctx.TEST_TAGS)
and ctx.should_run_test(target)):
with ctx.group(ctx.GRP_RUN_TESTS):
task = run_py_test(env=ctx.env, timeout=timeout)
task.set_inputs(target)
if not ctx.options.only_failed or not ctx.get_test_state(task.test_id):
ctx.add_to_group(task)
@conf
def py_test(ctx, source):
def py_test(ctx, source, mypy='loose', **kwargs):
if not ctx.env.ENABLE_TEST:
return
old_grp = ctx.current_group
ctx.set_group('tests')
try:
ctx.py_module(source)
finally:
ctx.set_group(old_grp)
with ctx.group(ctx.GRP_BUILD_TESTS):
target = ctx.py_module(source, mypy=mypy)
ctx.add_py_test_runner(target, **kwargs)

@ -45,11 +45,16 @@ def copy_file(task):
@conf
def static_file(ctx, source, install=None, install_to=None, rewrite=False, chmod=0o644):
def static_file(
ctx, source, target=None, install=None, install_to=None, rewrite=False, chmod=0o644):
if not isinstance(source, Node):
source = ctx.path.make_node(source)
target = source.get_bld()
if target is None:
target = source.get_bld()
if not isinstance(target, Node):
target = ctx.path.make_node(target).get_bld()
ctx(rule=copy_file,
source=source,
@ -57,7 +62,7 @@ def static_file(ctx, source, install=None, install_to=None, rewrite=False, chmod
rewrite=rewrite)
if install is None:
install = (ctx.get_group_name(ctx.current_group) == 'noisicaa')
install = ctx.in_group(ctx.GRP_BUILD_MAIN)
if install:
if install_to is None:

@ -43,7 +43,7 @@ def stripped_svg(ctx, source):
source=ctx.path.make_node(source),
target=target)
if ctx.get_group_name(ctx.current_group) == 'noisicaa':
if ctx.in_group(ctx.GRP_BUILD_MAIN):
ctx.install_files(
os.path.join(ctx.env.DATADIR, target.parent.path_from(ctx.bldnode.make_node('data'))),
target)

@ -0,0 +1,332 @@
# -*- mode: python -*-
# @begin:license
#
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# 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, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# @end:license
import datetime
import fnmatch
import glob
import os
import os.path
import shutil
import sys
import unittest
from waflib.Configure import conf
from waflib import Logs
ALL_TAGS = {'all', 'unit', 'lint', 'pylint', 'mypy', 'clang-tidy', 'integration', 'perf'}
def options(ctx):
grp = ctx.add_option_group('Test options')
grp.add_option(
'--tags',
default='unit,lint',
help=("Comma separated list of test classes to run (%s) [default: unit,lint]"
% ', '.join(sorted(ALL_TAGS))))
grp.add_option(
'--tests',
action='append',
default=None,
help=("Tests to run. Uses a prefix match and can contain globs. This flag can be used"
" multiple times [default: all tests]"))
grp.add_option(
'--fail-fast',
action='store_true',
default=False,
help="Abort test run on first failure.")
grp.add_option(
'--only-failed',
action='store_true',
default=False,
help="Only tests, which previously failed.")
grp.add_option(
'--coverage',
action='store_true',
default=False,
help="Enable code coverage report.")
@conf
def init_test(ctx):
if ctx.cmd == 'test':
if not ctx.env.ENABLE_TEST:
ctx.fatal("noisicaä has been configured without --enable-tests")
ctx.TEST_STATE_PATH = os.path.join(ctx.out_dir, 'teststates')
ctx.TEST_RESULTS_PATH = os.path.join(ctx.out_dir, 'testresults')
ctx.TEST_TAGS = set(ctx.options.tags.split(','))
for tag in ctx.TEST_TAGS:
assert tag in ALL_TAGS
ctx.add_pre_fun(test_init)
ctx.add_post_fun(test_complete)
def test_init(ctx):
if os.path.isdir(ctx.TEST_RESULTS_PATH):
shutil.rmtree(ctx.TEST_RESULTS_PATH)
if not ctx.options.only_failed and os.path.isdir(ctx.TEST_STATE_PATH):
shutil.rmtree(ctx.TEST_STATE_PATH)
def test_complete(ctx):
ctx.tests_failed = False
ctx.collect_unittest_results()
if {'all', 'lint', 'mypy'} & ctx.TEST_TAGS:
ctx.collect_mypy_results()
if {'all', 'lint', 'pylint'} & ctx.TEST_TAGS:
ctx.collect_pylint_results()
if {'all', 'lint', 'clang-tidy'} & ctx.TEST_TAGS:
ctx.collect_clang_tidy_results()
if ctx.options.coverage:
ctx.collect_coverage_results()
if ctx.tests_failed:
ctx.fatal("Some tests failed")
@conf
def should_run_test(ctx, path):
if not ctx.options.tests:
return True
mparts = path.relpath().split(os.sep)
for selector in ctx.options.tests:
sparts = selector.rstrip(os.sep).split(os.sep)
if len(sparts) > len(mparts):
continue
matched = True
for mpart, spart in zip(mparts[:len(sparts)], sparts):
if not fnmatch.fnmatch(mpart, spart):
matched = False
if matched:
return True
@conf
def record_test_state(ctx, test_name, state):
state_path = os.path.join(ctx.TEST_STATE_PATH, test_name)
os.makedirs(os.path.dirname(state_path), exist_ok=True)
with open(state_path, 'w') as fp:
fp.write('1' if state else '0')
@conf
def get_test_state(ctx, test_name):
state_path = os.path.join(ctx.TEST_STATE_PATH, test_name)
if os.path.isfile(state_path):
with open(state_path, 'r') as fp:
return bool(int(fp.read()))
return False
@conf
def collect_unittest_results(ctx):
import xunitparser
class TestCase(xunitparser.TestCase):
# This override only exists, because the original has a docstring, which shows up in the
# output...
def runTest(self): # pylint: disable=useless-super-delegation
super().runTest()
class TestSuite(xunitparser.TestSuite):
# Prevent tests from being None'ed in run().
_cleanup = False
class TextTestResult(unittest.TextTestResult, xunitparser.TestResult):
def addSuccess(self, test):
if self.showAll and test.time is not None:
self.stream.write('[%dms] ' % (test.time / datetime.timedelta(milliseconds=1)))
super().addSuccess(test)
class Parser(xunitparser.Parser):
TC_CLASS = TestCase
TS_CLASS = TestSuite
def flatten_suite(suite):
for child in suite:
if isinstance(child, unittest.TestSuite):
yield from flatten_suite(child)
else:
yield child
all_tests = unittest.TestSuite()
total_time = datetime.timedelta()
for result_path in glob.glob(os.path.join(ctx.TEST_RESULTS_PATH, '*', 'results.xml')):
if os.path.getsize(result_path) == 0:
continue
try:
ts, _ = Parser().parse(result_path)
for tc in flatten_suite(ts):
all_tests.addTest(tc)
if tc.time is not None:
total_time += tc.time
except Exception:
print("Failed to parse %s" % result_path)
raise
if not list(all_tests):
return
sorted_tests = unittest.TestSuite()
for tc in sorted(all_tests, key=lambda tc: (tc.classname, tc.methodname)):
sorted_tests.addTest(tc)
stream = unittest.runner._WritelnDecorator(sys.stderr)
result = TextTestResult(stream, True, verbosity=2)
result.startTestRun()
try:
sorted_tests(result)
finally:
result.stopTestRun()
result.printErrors()
stream.writeln(result.separator2)
run = result.testsRun
stream.writeln("Ran %d test%s in %s" %
(run, run != 1 and "s" or "", total_time))
stream.writeln()
infos = []
if not result.wasSuccessful():
msg = "FAILED"
if result.failures:
infos.append("failures=%d" % len(result.failures))
if result.errors:
infos.append("errors=%d" % len(result.errors))
else:
msg = "OK"
if result.skipped:
infos.append("skipped=%d" % len(result.skipped))
if result.expectedFailures:
infos.append("expected failures=%d" % len(result.expectedFailures))
if result.unexpectedSuccesses:
infos.append("unexpected successes=%d" % len(result.unexpectedSuccesses))
if infos:
msg += " (%s)" % ", ".join(infos)
if not result.wasSuccessful():
Logs.info(Logs.colors.RED + msg)
ctx.tests_failed = True
else:
Logs.info(msg)
@conf
def collect_mypy_results(ctx):
Logs.info(Logs.colors.BLUE + "Collecting mypy data...")
issues_found = False
for result_path in glob.glob(os.path.join(ctx.TEST_RESULTS_PATH, '*', 'mypy.log')):
with open(result_path, 'r') as fp:
log = fp.read()
if log:
issues_found = True
sys.stderr.write(log)
sys.stderr.write('\n\n')
if issues_found:
ctx.tests_failed = True
Logs.info(Logs.colors.RED + "mypy found some issues")
else:
Logs.info(Logs.colors.GREEN + "No issues found")
Logs.info('')
@conf
def collect_pylint_results(ctx):
Logs.info(Logs.colors.BLUE + "Collecting pylint data...")
issues_found = False
for result_path in glob.glob(os.path.join(ctx.TEST_RESULTS_PATH, '*', 'pylint.log')):
with open(result_path, 'r') as fp:
log = fp.read()
if log:
issues_found = True
sys.stderr.write(log)
sys.stderr.write('\n\n')
if issues_found:
ctx.tests_failed = True
Logs.info(Logs.colors.RED + "pylint found some issues")
else:
Logs.info(Logs.colors.GREEN + "No issues found")
Logs.info('')
@conf
def collect_clang_tidy_results(ctx):
Logs.info(Logs.colors.BLUE + "Collecting clang-tidy data...")
issues_found = False
for result_path in glob.glob(os.path.join(ctx.TEST_RESULTS_PATH, '*', 'clang-tidy.log')):
with open(result_path, 'r') as fp:
log = fp.read()
if log:
issues_found = True
sys.stderr.write(log)
sys.stderr.write('\n\n')
if issues_found:
ctx.tests_failed = True
Logs.info(Logs.colors.RED + "clang-tidy found some issues")
else:
Logs.info(Logs.colors.GREEN + "No issues found")
Logs.info('')
@conf
def collect_coverage_results(ctx):
Logs.info(Logs.colors.BLUE + "Collecting coverage data...")
import coverage
cov = coverage.Coverage()
data = cov.get_data()
for result_path in glob.glob(os.path.join(ctx.TEST_RESULTS_PATH, '*', 'coverage.data')):
d = coverage.CoverageData()
d.read_file(result_path)
data.update(d)
report_path = os.path.join(ctx.TEST_RESULTS_PATH, 'coverage')
total_coverage = cov.html_report(directory=report_path)
Logs.info(Logs.colors.GREEN + "Total coverage: %.1f%%" % total_coverage)
Logs.info(Logs.colors.GREEN + "Coverage report: file://%s/index.html" % report_path)
Logs.info('')

@ -116,6 +116,9 @@ def configure(ctx):
os_release = Version(os_release)
ctx.end_msg("%s %s" % (os_dist, os_release))
ctx.env.OS_DIST = os_dist
ctx.env.OS_RELEASE = str(os_release)
ctx.start_msg("Query pip for installed packages")
pip_mgr = PipManager(ctx)
pip_mgr.update_packages()
@ -145,15 +148,20 @@ def configure(ctx):
pip_mgr.check_package(BUILD, 'pip', version='>=19.0')
pip_mgr.check_package(BUILD, 'setuptools', version='>=41.0')
pip_mgr.check_package(BUILD, 'wheel', version='>=0.33')
sys_mgr.check_package(BUILD, 'build-essential')
sys_mgr.check_package(BUILD, 'python3-dev')
# Misc pip packages:
pip_mgr.check_package(RUNTIME, 'eventfd')
pip_mgr.check_package(RUNTIME, 'lucky-humanize')
pip_mgr.check_package(RUNTIME, 'numpy')
# numpy.core.numeric.asarray is gone from numpy 1.17
pip_mgr.check_package(RUNTIME, 'numpy', version='1.16.4')
pip_mgr.check_package(RUNTIME, 'portalocker')
pip_mgr.check_package(RUNTIME, 'posix-ipc')
pip_mgr.check_package(RUNTIME, 'psutil')
pip_mgr.check_package(RUNTIME, 'pyparsing')
# psutil 5.6.3 has an empty __init__.py...
pip_mgr.check_package(RUNTIME, 'psutil', version='5.6.2')
# pyparsing 2.4.1 causes issues with packaging
pip_mgr.check_package(RUNTIME, 'pyparsing', version='2.4.0')
pip_mgr.check_package(RUNTIME, 'sortedcontainers')
pip_mgr.check_package(RUNTIME, 'toposort')
pip_mgr.check_package(RUNTIME, 'urwid')
@ -162,6 +170,7 @@ def configure(ctx):
pip_mgr.check_package(BUILD, 'Jinja2')
pip_mgr.check_package(BUILD, 'PyYAML')
pip_mgr.check_package(BUILD, 'packaging', version='>=19.0')
pip_mgr.check_package(BUILD, 'xunitparser')
pip_mgr.check_package(DEV, 'asynctest')
pip_mgr.check_package(DEV, 'async-generator')
pip_mgr.check_package(DEV, 'coverage')
@ -169,9 +178,11 @@ def configure(ctx):
pip_mgr.check_package(DEV, 'py-cpuinfo')
pip_mgr.check_package(DEV, 'pyfakefs')
pip_mgr.check_package(DEV, 'pylint', version='2.3.1')
pip_mgr.check_package(DEV, 'unittest-xml-reporting')
# misc sys packages:
sys_mgr.check_package(RUNTIME, 'ffmpeg')
sys_mgr.check_package(RUNTIME, 'libxkbcommon-x11-0')
sys_mgr.check_package(BUILD, 'cmake')
sys_mgr.check_package(BUILD, 'python3-dev')
sys_mgr.check_package(BUILD, 'portaudio19-dev')
@ -183,10 +194,16 @@ def configure(ctx):
sys_mgr.check_package(DEV, 'xvfb')
sys_mgr.check_package(DEV, 'intltool')
# git is needed to fetch PIP packages from 'git+https://...' sources.
sys_mgr.check_package(BUILD, 'git')
# mypy
pip_mgr.check_package(DEV, 'mypy', version='0.701')
pip_mgr.check_package(DEV, 'mypy', version='0.720')
pip_mgr.check_package(RUNTIME, 'mypy-extensions')
# sndfile
sys_mgr.check_package(BUILD, 'libsndfile1-dev')
# csound
sys_mgr.check_package(BUILD, 'libsamplerate0-dev')
sys_mgr.check_package(BUILD, 'libboost-dev')
@ -214,9 +231,6 @@ def configure(ctx):
FaustBuilder(ctx).check(BUILD, version='2.15.11')
FaustLibrariesBuilder(ctx).check(BUILD, version='64a57f56') # snapshot from 2019-03-30
# sndfile
sys_mgr.check_package(BUILD, 'libsndfile1-dev')
# libswresample
sys_mgr.check_package(BUILD, 'libswresample-dev')
@ -256,21 +270,34 @@ def configure(ctx):
pip_mgr.check_package(BUILD, 'mypy-protobuf', source='git+https://github.com/odahoda/mypy-protobuf.git#egg=mypy-protobuf&subdirectory=python')
# profiling
sys_mgr.check_package(DEV, 'google-perftools')
sys_mgr.check_package(RUNTIME, 'google-perftools')
sys_mgr.check_package(RUNTIME, 'libgoogle-perftools4')
sys_mgr.check_package(BUILD, 'libgoogle-perftools-dev')
# indicator-cpufreq
sys_mgr.check_package(DEV, 'libdbus-1-dev')
pip_mgr.check_package(DEV, 'dbus-python')
sys_mgr.check_package(DEV, 'bzr')
pip_mgr.check_package(DEV, 'python-distutils-extra', source='bzr+lp:python-distutils-extra#egg=python-distutils-extra')
pip_mgr.check_package(DEV, 'indicator-cpufreq', source='bzr+lp:indicator-cpufreq#egg=indicator-cpufreq')
sys_mgr.check_package(DEV, 'indicator-cpufreq')
# clang-tidy
if ctx.env.ENABLE_TEST:
# clang-tidy is optional, let's install it on systems where we known that V8 is
# available. On other systems it's up to the user to install it.
if os_dist == 'ubuntu' and os_release >= Version('18.04'):
sys_mgr.check_package(DEV, 'clang-tidy-8')
# vmtest
pip_mgr.check_package(VMTEST, 'paramiko')
pip_mgr.check_package(VMTEST, 'python-xlib')
sys_mgr.check_package(VMTEST, 'virtualbox')
sys_mgr.check_package(VMTEST, 'qemu-system-x86')
sys_mgr.check_package(VMTEST, 'qemu-block-extra')
sys_mgr.check_package(VMTEST, 'libvirt-bin')
pip_mgr.check_package(VMTEST, 'asyncssh')
sys_mgr.check_package(VMTEST, 'sshpass')
sys_mgr.check_package(VMTEST, 'openssh-client')
pip_mgr.check_package(VMTEST, 'aiohttp')
AptCacherNGBuilder(ctx).check(VMTEST, version='3.2')
# pylint: enable=line-too-long
@ -303,6 +330,14 @@ def check_virtual_env(ctx):
except Exception as exc: # pylint: disable=broad-except
shutil.rmtree(venvdir)
ctx.fatal("Failed to create virtual env: %s" % exc)
# Always update PIP to something more recent than what ensurepip has installed. We need at
# least 9.0 for 'pip list --format=json' to work.
ctx.cmd_and_log(
[os.path.join(venvdir, 'bin', 'pip'),
'--disable-pip-version-check', 'install', '-U', 'pip>=9.0'],
output=BOTH)
ctx.to_log(" ok.")
old_venvdir = None
@ -317,6 +352,7 @@ def check_virtual_env(ctx):
ctx.to_log(" ok.")
ctx.env.VIRTUAL_ENV = venvdir
ctx.env.PYTHON = [os.path.join(venvdir, 'bin', 'python')]
ctx.environ['PATH'] = os.pathsep.join(
[os.path.join(venvdir, 'bin')] + ctx.environ.get('PATH', '').split(os.pathsep))
@ -431,7 +467,7 @@ class PipManager(PackageManager):
p = subprocess.run(
self.__pip_cmd + ['list', '--format=json'],
stdout=subprocess.PIPE, check=True)
self._packages = {p['name']: p['version'] for p in json.loads(p.stdout)}
self._packages = {p['name']: p['version'] for p in json.loads(p.stdout.decode('utf-8'))}
def get_pip_spec(self, name, version=None, source=None):
if source:
@ -491,7 +527,7 @@ class DebManager(PackageManager):
cmd += ['--askpass']
else:
cmd += ['--non-interactive']
cmd += ['--', '/usr/bin/apt-get', 'install', name]
cmd += ['--', '/usr/bin/apt-get', '-q', '-y', 'install', name]
self._ctx.cmd_and_log(cmd, output=BOTH, env=env)
@ -669,7 +705,7 @@ class CSoundBuilder(ThirdPartyBuilder):
],
cwd=make_path)
self._ctx.cmd_and_log(
['make', '-j8'],
['make', '-j%d' % len(os.sched_getaffinity(0))],
cwd=make_path)
def install(self, src_path):
@ -689,7 +725,7 @@ class FaustBuilder(ThirdPartyBuilder):
def build(self, src_path):
self._ctx.cmd_and_log(
['make',
'-j8',
'-j%d' % len(os.sched_getaffinity(0)),
'PREFIX=' + self._ctx.env.VIRTUAL_ENV,
'compiler'],
cwd=src_path)
@ -831,10 +867,31 @@ class ProtocBuilder(ThirdPartyBuilder):
],
cwd=src_path)
self._ctx.cmd_and_log(
['make', '-j8'],
['make', '-j%d' % len(os.sched_getaffinity(0))],
cwd=src_path)
def install(self, src_path):
self._ctx.cmd_and_log(
['make', 'install'],
cwd=src_path)
class AptCacherNGBuilder(ThirdPartyBuilder):
def __init__(self, ctx):
super().__init__(ctx, 'apt-cacher-ng', '.tar.xz')
def download_url(self, version):
# pylint: disable=line-too-long
return 'http://ftp.debian.org/debian/pool/main/a/apt-cacher-ng/apt-cacher-ng_%s.orig.tar.xz' % version
def build(self, src_path):
self._ctx.cmd_and_log(
['./build.sh',
'-DCMAKE_INSTALL_PREFIX=%s' % self._ctx.env.VIRTUAL_ENV,
],
cwd=src_path)
def install(self, src_path):
self._ctx.cmd_and_log(
['make', '-C', 'builddir', 'install'],
cwd=src_path)

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="64" version="1.1" xmlns="http://www.w3.org/2000/svg" height="64" xmlns:xlink="http://www.w3.org/1999/xlink"