Add check-strings (#6379)

This adds a script `check-strings` that checks whether strings, such as file paths or class names, are valid in files outside of the source code, e.g. in translations or themes. This also adds a verify script to verify `check-strings` on a constant git commit. Both scripts are under CI.
This commit is contained in:
Johannes Lorenz
2022-05-23 20:35:06 +02:00
committed by GitHub
parent 3964c53a0b
commit 230aece217
43 changed files with 6732 additions and 6405 deletions

View File

@@ -0,0 +1,15 @@
# Check strings
This tool checks for invalid strings not necessarily in the code.
Background: When you move a lot of classes and files, the code may be OK, but
many strings outside the code (translations, style.css, .gitmodules etc.) may
still need to be updated. This script checks whether these strings are still
matching the code.
Content:
* `check-strings`: Script to check for invalid strings
* `verify`: Script verifying `check-strings`
* expectation.txt: Expectation file for verify script

160
tests/check-strings/check-strings Executable file
View File

@@ -0,0 +1,160 @@
#!/usr/bin/python3
# This script checks for strings like paths or class names that are *not* in the source code, but e.g. in
# translation files, stylesheets or git files.
# Invalid strings *in the source code* are usually recognized when you compile them, but other strings may
# be overseen, which is why this script checks strings *outside of the source code*.
import re
import subprocess
import xml.etree.ElementTree as ElementTree
from pathlib import Path
import tinycss2
# global variables
errors = 0
# functions
def caption(my_str):
print(f'\n# {my_str}\n')
def error(where, my_str):
global errors
errors += 1
print(f'Error: {where}: {my_str}')
# if a string makes a classname, we need to check if that class is in the source
# however, some of these strings name classes that are not from LMMS, so these can be ignored for such checks:
def is_our_class(classname: str) -> int:
return classname[0] != 'Q' # Qt classes
# prepare some variables
if not Path('.gitmodules').is_file():
print('You need to call this script from the LMMS top directory')
exit(1)
result = subprocess.run(['git', 'ls-files', '*.[ch]', '*.[ch]pp', '*.ui', ':!tests/*'],
capture_output=True, text=True, check=True)
files = [Path(f) for f in result.stdout.splitlines()]
carlabase = 'carlabase' if Path('plugins/carlabase').is_dir() else 'CarlaBase'
carlapath = f'plugins/{carlabase}/carla/'
result = subprocess.run(['git',
'--git-dir', f'{carlapath}/.git',
'--work-tree', f'{carlapath}',
'ls-files', 'resources/ui', 'source/frontend'], capture_output=True, text=True, check=True)
files.extend([Path(f'{carlapath}/{f}') for f in result.stdout.splitlines()])
classes = set()
class_pat = re.compile(r'^\s*class(?:\s+LMMS_EXPORT)?\s+([a-zA-Z_][a-zA-Z0-9_]*)', re.MULTILINE)
class_pat_ui = re.compile(r'<class>([a-zA-Z_][a-zA-Z0-9_]*)</class>')
for cur_file in files:
if cur_file.is_file():
text = cur_file.read_text(errors='replace')
classes.update(re.findall(class_pat_ui if cur_file.suffix == '.ui' else class_pat, text))
# the real checks
caption('.gitmodules')
for p in re.findall(r'\[submodule "([^"]+)"\]\s*$', Path('.gitmodules').read_text(errors='replace'), re.MULTILINE):
if not Path(p).is_dir():
error('.gitmodules', f'Directory does not exist: {p}')
caption('locale')
filenames = set()
classes_found = set()
for cur_file in Path('data/locale').glob('*.ts'):
tree = ElementTree.parse(str(cur_file))
root = tree.getroot()
for location in root.findall('./context/message/location'):
filenames.add(location.attrib['filename'])
for location in root.findall('./context/name'):
classes_found.add(location.text)
for f in sorted(filenames):
# The files sometimes are relative to data/local and sometimes to the git tree's root...
if not Path(f).is_file() and not Path(f'data/locale/{f}').is_file():
error('data/locale', f'Source file does not exist: {f}')
for c in sorted(classes_found):
if is_our_class(c) and '::' not in c and c not in classes:
error('data/locale', f'Class does not exist in source code: {c}')
caption('themes')
for theme in sorted([d for d in Path('data/themes').iterdir() if d.is_dir()]):
classes_in_sheet = set()
stylesheet = theme / 'style.css'
rules = tinycss2.parse_stylesheet(Path(stylesheet).read_text(errors='replace'))
for rule in rules:
if rule.type == 'qualified-rule':
class_found = False
for c in rule.prelude:
if c.type == 'ident' and not class_found:
if is_our_class(c.value):
classes_in_sheet.add(c.value)
class_found = True
# After whitespace or comma comes a new class
elif c.type == 'whitespace' or (c.type == 'literal' and c.value == ','):
class_found = False
missing_classes = classes_in_sheet - classes
for class_in_sheet in sorted(missing_classes):
error(str(stylesheet), f'Class does not exist in source code: {class_in_sheet}')
caption('patches (checks only plugins/)')
pat = re.compile(r'/(plugins/\S*)', re.MULTILINE)
calf = re.compile(r'calf/.*/modules\.') # these are a bit complicated to fix...
for cur_file in sorted(Path('.').glob('*/patches/*.patch')):
if Path(cur_file).is_file():
paths_in_patches = set()
for line in pat.findall(cur_file.read_text(errors='replace')):
if not calf.search(line):
paths_in_patches.add(Path(line))
for mpath in sorted(paths_in_patches):
# in case of LADSPA SWH effects, check that the XML exists, not the C file
# (because the C files are not generated until a build is done)
if mpath.parent == Path('plugins/LadspaEffect/swh/ladspa/'):
mpath = mpath.with_suffix('.xml')
if not mpath.is_file():
error(str(cur_file), f'Source file does not exist: {str(mpath)}')
caption('debian docs (only one string)')
# Checks for caps.html. This gets relevant when #4027 will be merged
for line in Path('debian/lmms-common.docs').read_text(errors='replace').splitlines():
line = line.rstrip()
if 'caps.html' in line and not Path(line).is_file():
error('debian/lmms-common.docs', f'Path does not exist: {line}')
caption('debian/copyright')
pat = re.compile(r'^Files:\s*(\S+).*$', re.MULTILINE)
ladspa_swh = re.compile(r'(plugins/LadspaEffect/swh/ladspa/[^/.]+)\.c')
for mpath in pat.findall(Path('debian/copyright').read_text(errors='replace')):
# in case of LADSPA SWH effects, check that the XML exists, not the C file
# (because the C files are not generated until a build is done)
if res2 := ladspa_swh.match(mpath):
mpath = res2.group(1) + '.xml'
if not any(Path('.').glob(mpath)):
error('debian/copyright', f'Glob/Path does not exist: {mpath}')
# summary
caption('summary')
print(f'{str(errors)} errors.')
exit(1 if errors > 0 else 0)

View File

@@ -0,0 +1,81 @@
# .gitmodules
Error: .gitmodules: Directory does not exist: plugins/sid/resid
# locale
Error: data/locale: Source file does not exist: ../../src/gui/BBClipView.cpp
Error: data/locale: Source file does not exist: ../../src/gui/editors/BBEditor.cpp
Error: data/locale: Source file does not exist: ../../src/tracks/BBTrack.cpp
Error: data/locale: Source file does not exist: plugins/SpectrumAnalyzer/SpectrumAnalyzer.cpp
Error: data/locale: Source file does not exist: plugins/SpectrumAnalyzer/SpectrumAnalyzerControlDialog.cpp
Error: data/locale: Source file does not exist: plugins/SpectrumAnalyzer/SpectrumAnalyzerControls.cpp
Error: data/locale: Source file does not exist: plugins/flp_import/FlpImport.cpp
Error: data/locale: Source file does not exist: plugins/opl2/opl2instrument.cpp
Error: data/locale: Source file does not exist: plugins/papu/papu_instrument.cpp
Error: data/locale: Source file does not exist: plugins/sid/sid_instrument.cpp
Error: data/locale: Source file does not exist: src/gui/editors/BBEditor.cpp
Error: data/locale: Source file does not exist: src/gui/widgets/VisualizationWidget.cpp
Error: data/locale: Source file does not exist: src/tracks/BBTrack.cpp
Error: data/locale: Class does not exist in source code: BBClipView
Error: data/locale: Class does not exist in source code: BBEditor
Error: data/locale: Class does not exist in source code: BBTrack
Error: data/locale: Class does not exist in source code: SpectrumAnalyzerControlDialog
Error: data/locale: Class does not exist in source code: SpectrumAnalyzerControls
Error: data/locale: Class does not exist in source code: VisualizationWidget
Error: data/locale: Class does not exist in source code: mixerLineLcdSpinBox
Error: data/locale: Class does not exist in source code: opl2instrument
Error: data/locale: Class does not exist in source code: opl2instrumentView
Error: data/locale: Class does not exist in source code: papuInstrument
Error: data/locale: Class does not exist in source code: papuInstrumentView
Error: data/locale: Class does not exist in source code: pluginBrowser
Error: data/locale: Class does not exist in source code: sidInstrument
Error: data/locale: Class does not exist in source code: sidInstrumentView
# themes
Error: data/themes/classic/style.css: Class does not exist in source code: PluginDescList
Error: data/themes/classic/style.css: Class does not exist in source code: effectLabel
Error: data/themes/classic/style.css: Class does not exist in source code: nameLabel
Error: data/themes/classic/style.css: Class does not exist in source code: opl2instrumentView
Error: data/themes/classic/style.css: Class does not exist in source code: sidInstrumentView
Error: data/themes/default/style.css: Class does not exist in source code: PluginDescList
Error: data/themes/default/style.css: Class does not exist in source code: effectLabel
Error: data/themes/default/style.css: Class does not exist in source code: nameLabel
Error: data/themes/default/style.css: Class does not exist in source code: opl2instrumentView
Error: data/themes/default/style.css: Class does not exist in source code: sidInstrumentView
# patches (checks only plugins/)
Error: debian/patches/clang.patch: Source file does not exist: plugins/LadspaEffect/calf/src/calf/metadata.h
Error: debian/patches/clang.patch: Source file does not exist: plugins/LadspaEffect/calf/src/calf/modules_comp.h
Error: debian/patches/clang.patch: Source file does not exist: plugins/LadspaEffect/calf/src/calf/modules_limit.h
Error: debian/patches/clang.patch: Source file does not exist: plugins/LadspaEffect/calf/src/calf/modules_mod.h
Error: debian/patches/clang.patch: Source file does not exist: plugins/LadspaEffect/calf/src/calf/organ.h
Error: debian/patches/clang.patch: Source file does not exist: plugins/LadspaEffect/calf/src/calf/preset.h
Error: debian/patches/clang.patch: Source file does not exist: plugins/LadspaEffect/calf/src/calf/primitives.h
Error: debian/patches/clang.patch: Source file does not exist: plugins/LadspaEffect/calf/src/metadata.cpp
Error: debian/patches/clang.patch: Source file does not exist: plugins/LadspaEffect/swh/flanger_1191.c
Error: debian/patches/clang.patch: Source file does not exist: plugins/LadspaEffect/swh/gsm/short_term.c
Error: debian/patches/clang.patch: Source file does not exist: plugins/LadspaEffect/swh/multivoice_chorus_1201.c
Error: debian/patches/clang.patch: Source file does not exist: plugins/LadspaEffect/swh/retro_flange_1208.c
Error: debian/patches/clang.patch: Source file does not exist: plugins/LadspaEffect/swh/vynil_1905.c
Error: debian/patches/clang.patch: Source file does not exist: plugins/delay/stereodelay.cpp
Error: debian/patches/clang.patch: Source file does not exist: plugins/opl2/fmopl.c
# debian docs (only one string)
# debian/copyright
Error: debian/copyright: Glob/Path does not exist: data/projects/CoolSongs/Saber-*
Error: debian/copyright: Glob/Path does not exist: plugins/LadspaEffect/calf/src/calf/vumeter.h
Error: debian/copyright: Glob/Path does not exist: plugins/LadspaEffect/swh/gsm/*
Error: debian/copyright: Glob/Path does not exist: plugins/LadspaEffect/swh/util/pitchscale.c
Error: debian/copyright: Glob/Path does not exist: plugins/LadspaEffect/swh/vocoder_1337.c
Error: debian/copyright: Glob/Path does not exist: plugins/opl2/fmopl.*
# summary
59 errors.

60
tests/check-strings/verify Executable file
View File

@@ -0,0 +1,60 @@
#!/usr/bin/python3
import subprocess
from pathlib import Path
import tempfile
import os
lmms_main_path = Path(__file__).resolve().parent.parent.parent
res = subprocess.run(['git', 'rev-parse', 'HEAD'], capture_output=True, text=True, check=True)
head = res.stdout.rstrip()
# test with a stable LMMS ref to always get the same expectations
ref = 'f56fc68b669b59f525ad133a0376feaba2a1a4ec'
# find out a remote URL where we can fetch "ref" from
res = subprocess.run(['git', 'rev-parse', '-q', '--verify', ref+'^{commit}'], capture_output=True, text=True)
if res.returncode == 0:
remote = str(lmms_main_path)
else:
res = subprocess.run(['git', 'remote', 'get-url', 'origin'], capture_output=True, text=True)
if res.returncode == 0:
remote = res.stdout.rstrip()
else:
res = subprocess.run(['git', 'remote', 'get-url', 'upstream'], capture_output=True, text=True)
if res.returncode == 0:
remote = res.stdout.rstrip()
else:
raise RuntimeError(
"Sorry, I can find ref " + ref + " neither in the repo nor in the remotes \"origin\" and \"upstream\"")
with tempfile.TemporaryDirectory() as tmpdir:
os.chdir(tmpdir)
# get the repo and its submodules
subprocess.run(['git', 'init'], check=True)
subprocess.run(['git', 'remote', 'add', 'origin', remote], check=True)
subprocess.run(['git', 'fetch', '--depth=1', 'origin', ref, head], check=True)
subprocess.run(['git', 'checkout', ref], check=True)
subprocess.run(['git', 'submodule', 'update', '--depth=1', '--init'], check=True)
# checkout the CURRENT check-strings, because it is subject-under-test
subprocess.run(['git', 'checkout', head, 'tests/check-strings/check-strings'], check=True)
# run script
res = subprocess.run([str(lmms_main_path / 'tests' / 'check-strings' / 'check-strings')],
capture_output=True, text=True)
print('--->8--- Script output BEGIN --->8---')
print(res.stdout)
print('--->8--- Script output END --->8---')
if res.stderr:
print('--->8--- Script error output BEGIN --->8---')
print(res.stderr)
print('--->8--- Script error output END --->8---')
# make sure script returned "error" (because we test for errors) and that the output is as expected
if res.returncode == 0:
raise RuntimeError("Script \"check-strings\" no errors, but errors were expected")
else:
if res.stdout != open(str(lmms_main_path / 'tests' / 'check-strings' / 'expectation.txt')).read():
raise RuntimeError("Script \"check-strings\" returned with different output than in expectation.txt")
# if we made it until here without an exception, all tests have been passed
print("SUCCESS")