Test suite started.

This commit is contained in:
Erik Moqvist
2026-05-28 19:24:41 +02:00
parent 9902d3b1b9
commit c614777606
10 changed files with 148 additions and 117 deletions
+6
View File
@@ -9,6 +9,12 @@ jobs:
runs-on: macos-26
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install Python dependencies
run: |
pip install -r requirements.txt
- name: Create user config
run: |
cp User.template.xcconfig Config/User.xcconfig
+2 -2
View File
@@ -98,5 +98,5 @@ Config/User.xcconfig
# Node.js
node_modules/
utils/all.*
utils/logs
test/all.*
test/logs
+12 -5
View File
@@ -7,6 +7,10 @@ SWIFTFORMAT_ARGS = \
SWIFTLINT_ARGS = --strict --quiet
OXFMT_ARGS = "WebRemoteControlFrontend"
OXLINT_ARGS = "WebRemoteControlFrontend"
PYTHON_DIRS = \
test \
utils
BLACK_ARGS = $(PYTHON_DIRS)
PERIPHERY_ARGS = \
--index-exclude "Moblin/Integrations/Tesla/Protobuf/*" \
--index-exclude "**/PrepareLicenseList/**" \
@@ -21,8 +25,9 @@ PYLINT_ARGS = \
--disable broad-exception-caught \
--disable too-many-locals \
--disable duplicate-code \
--disable missing-class-docstring \
--recursive yes \
.
$(PYTHON_DIRS)
CODE_FOLDERS += "Common"
CODE_FOLDERS += "Moblin"
@@ -35,24 +40,26 @@ CODE_FOLDERS += "WebRemoteControlFrontend"
SHELL = /usr/bin/env bash
.PHONY: test
default:
style:
swiftformat $(CODE_FOLDERS) $(SWIFTFORMAT_ARGS)
oxfmt $(OXFMT_ARGS)
black $(BLACK_ARGS) || true
style-check:
swiftformat $(CODE_FOLDERS) $(SWIFTFORMAT_ARGS) --lint
oxfmt $(OXFMT_ARGS) --check
black $(BLACK_ARGS) --check
lint:
swiftlint lint $(SWIFTLINT_ARGS) $(CODE_FOLDERS)
oxlint $(OXLINT_ARGS)
pylint $(PYLINT_ARGS) || true
python3 utils/xcstringslint.py Common/Localizable.xcstrings
pylint:
pylint $(PYLINT_ARGS)
lint-fix:
python3 utils/xcstringslint.py --fix Common/Localizable.xcstrings
@@ -63,7 +70,7 @@ spell-check:
codespell $(CODESPELL_ARGS) $(CODE_FOLDERS)
test:
cd utils && python test.py
cd test && python test.py
machine-translate:
python3 utils/translate.py Common/Localizable.xcstrings
+4
View File
@@ -0,0 +1,4 @@
systest
black
pylint
deep_translator
+29 -13
View File
@@ -1,19 +1,28 @@
import logging
import systest
import subprocess
import time
import systest
REMOTE_CONTROL_PORT = '2345'
REMOTE_CONTROL_PORT = "2345"
LOGGER = logging.getLogger(__name__)
class Moblin:
def __init__(self):
self._server = None
def __enter__(self):
self._server = subprocess.Popen(["moblin_assistant",
"--port", REMOTE_CONTROL_PORT,
"run",
"--password", "1234"])
self._server = subprocess.Popen(
[
"moblin_assistant",
"--port",
REMOTE_CONTROL_PORT,
"run",
"--password",
"1234",
]
)
time.sleep(1)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
@@ -23,10 +32,15 @@ class Moblin:
self._execute("go_live")
def _execute(self, command):
subprocess.run(["moblin_assistant", "--port", REMOTE_CONTROL_PORT, command])
subprocess.run(
["moblin_assistant", "--port", REMOTE_CONTROL_PORT, command], check=True
)
class MediaMtx:
def __init__(self):
self._server = None
def __enter__(self):
self._server = subprocess.Popen(["mediamtx"])
return self
@@ -36,6 +50,9 @@ class MediaMtx:
class Ffmpeg:
def __init__(self):
self._server = None
def __enter__(self):
self._server = subprocess.Popen(["ffmpeg"])
return self
@@ -46,13 +63,12 @@ class Ffmpeg:
class RtmpFromMoblinToMediaMtx(systest.TestCase):
def __init__(self, moblin: Moblin):
super(RtmpFromMoblinToMediaMtx, self).__init__()
super().__init__()
self.moblin = moblin
def run(self):
time.sleep(5)
self.moblin.go_live()
time.sleep(1)
with MediaMtx():
self.moblin.go_live()
def main():
@@ -64,4 +80,4 @@ def main():
sequencer.report_and_exit()
main()
main()
+29 -34
View File
@@ -8,23 +8,23 @@ import argparse
def set_bitrate_and_loss(bitrate, loss):
if bitrate is None and loss is None:
print('Neither bitrate nor loss given. Aborting...')
print("Neither bitrate nor loss given. Aborting...")
return
print(time.ctime())
args = ''
args = ""
if bitrate is not None:
print(f" - Bitrate {bitrate} Mbit")
args += f' rate {bitrate}Mbit'
args += f" rate {bitrate}Mbit"
if loss is not None:
print(f" - Loss {loss} %")
args += f' loss {loss}%'
args += f" loss {loss}%"
subprocess.run('sudo tc qdisc replace dev eno1 root netem' + args,
shell=True,
check=True)
subprocess.run(
"sudo tc qdisc replace dev eno1 root netem" + args, shell=True, check=True
)
def do_constant(args):
@@ -49,47 +49,42 @@ def do_random(args):
def do_reset(_args):
subprocess.run('sudo tc qdisc del dev eno1 root', shell=True, check=True)
subprocess.run("sudo tc qdisc del dev eno1 root", shell=True, check=True)
def main():
parser = argparse.ArgumentParser()
# Workaround to make the subparser required in Python 3.
subparsers = parser.add_subparsers(title='subcommands',
dest='subcommand')
subparsers = parser.add_subparsers(title="subcommands", dest="subcommand")
subparsers.required = True
subparser = subparsers.add_parser('reset')
subparser = subparsers.add_parser("reset")
subparser.set_defaults(func=do_reset)
subparser = subparsers.add_parser('constant')
subparser.add_argument('--bitrate', type=float, help='Bitrate in Mbps.')
subparser.add_argument('--loss', type=float, help='Loss in %%.')
subparser = subparsers.add_parser("constant")
subparser.add_argument("--bitrate", type=float, help="Bitrate in Mbps.")
subparser.add_argument("--loss", type=float, help="Loss in %%.")
subparser.set_defaults(func=do_constant)
subparser = subparsers.add_parser('square')
subparser.add_argument('--low-bitrate',
default=2,
type=float,
help='Low bitrate in Mbps.')
subparser.add_argument('--high-bitrate',
default=10,
type=float,
help='High bitrate in Mbps.')
subparser.add_argument('--low-time',
default=15,
type=float,
help='Low bitrate time in seconds.')
subparser.add_argument('--high-time',
default=15,
type=float,
help='High bitrate time in seconds.')
subparser.add_argument('--loss', type=float, help='Loss in %%.')
subparser = subparsers.add_parser("square")
subparser.add_argument(
"--low-bitrate", default=2, type=float, help="Low bitrate in Mbps."
)
subparser.add_argument(
"--high-bitrate", default=10, type=float, help="High bitrate in Mbps."
)
subparser.add_argument(
"--low-time", default=15, type=float, help="Low bitrate time in seconds."
)
subparser.add_argument(
"--high-time", default=15, type=float, help="High bitrate time in seconds."
)
subparser.add_argument("--loss", type=float, help="Loss in %%.")
subparser.set_defaults(func=do_square)
subparser = subparsers.add_parser('random')
subparser.add_argument('--loss', type=float, help='Loss in %%.')
subparser = subparsers.add_parser("random")
subparser.add_argument("--loss", type=float, help="Loss in %%.")
subparser.set_defaults(func=do_random)
args = parser.parse_args()
+6 -5
View File
@@ -6,17 +6,18 @@ import argparse
def analyze(video):
pts_time_lines = subprocess.run(
f'ffprobe {video} -show_frames -hide_banner -loglevel warning -select_streams v:0 '
'| grep pts_time',
f"ffprobe {video} -show_frames -hide_banner -loglevel warning -select_streams v:0 "
"| grep pts_time",
text=True,
shell=True,
check=True,
capture_output=True).stdout.splitlines()
capture_output=True,
).stdout.splitlines()
prev_pts_time = None
for pts_time_line in pts_time_lines:
pts_time = float(pts_time_line.split('=')[1])
pts_time = float(pts_time_line.split("=")[1])
if prev_pts_time is not None:
delta_ms = 1000 * (pts_time - prev_pts_time)
@@ -27,7 +28,7 @@ def analyze(video):
def main():
parser = argparse.ArgumentParser()
parser.add_argument('video', help='The video to analyze.')
parser.add_argument("video", help="The video to analyze.")
args = parser.parse_args()
analyze(args.video)
+19 -18
View File
@@ -25,27 +25,27 @@ LANGUAGES = [
("ko", "ko"),
("ru", "ru"),
("uk", "uk"),
("sk", "sk")
("sk", "sk"),
]
def needs_translation(item):
state = item['stringUnit']['state']
state = item["stringUnit"]["state"]
return state not in ['translated', 'needs_review']
return state not in ["translated", "needs_review"]
def main():
localizable_xcstrings_path = Path(sys.argv[1])
localizable = json.loads(localizable_xcstrings_path.read_text(encoding='utf-8'))
localizable = json.loads(localizable_xcstrings_path.read_text(encoding="utf-8"))
try:
for english, value in localizable['strings'].items():
localizations = value.get('localizations')
for english, value in localizable["strings"].items():
localizations = value.get("localizations")
if localizations is None:
localizations = {}
value['localizations'] = localizations
value["localizations"] = localizations
for xcode_languages, google_language in LANGUAGES:
translated = None
@@ -61,8 +61,12 @@ def main():
continue
if translated is None:
print(f'Translating "{english}" to {", ".join(xcode_languages)}')
translator = GoogleTranslator(source='en', target=google_language)
print(
f'Translating "{english}" to {", ".join(xcode_languages)}'
)
translator = GoogleTranslator(
source="en", target=google_language
)
try:
translated = translator.translate(english)
@@ -70,18 +74,15 @@ def main():
translated = english
localizations[xcode_language] = {
'stringUnit': {
'state': 'needs_review',
'value': translated
}
"stringUnit": {"state": "needs_review", "value": translated}
}
finally:
localizable_xcstrings_path.write_text(
json.dumps(localizable,
indent=2,
ensure_ascii=False,
separators=(',', ' : ')),
encoding='utf-8')
json.dumps(
localizable, indent=2, ensure_ascii=False, separators=(",", " : ")
),
encoding="utf-8",
)
main()
+33 -31
View File
@@ -7,7 +7,7 @@ from pathlib import Path
# Matches iOS/Swift format specifiers with an optional positional prefix.
# Handles: %@, %lld, %llu, %d, %f, %u and positional forms like %1$@, %2$lld.
SPECIFIER_RE = re.compile(r'%(\d+\$)?(@|lld|llu|[dfu])')
SPECIFIER_RE = re.compile(r"%(\d+\$)?(@|lld|llu|[dfu])")
def extract_specifiers(s):
@@ -24,26 +24,26 @@ def check_format_specifiers(string_in_code, localized_string, language_code):
if code_types != loc_types:
errors.append(
f' [{language_code}] Format specifier mismatch: '
f'code={[t for _, t in code_specs]}, '
f'localized={[t for _, t in loc_specs]}'
f" [{language_code}] Format specifier mismatch: "
f"code={[t for _, t in code_specs]}, "
f"localized={[t for _, t in loc_specs]}"
)
elif len(code_specs) > 1:
if any(pos is None for pos, _ in loc_specs):
errors.append(
f' [{language_code}] Missing positional prefixes in: '
f'{localized_string}'
f" [{language_code}] Missing positional prefixes in: "
f"{localized_string}"
)
else:
n = len(code_specs)
positions = [int(pos.rstrip('$')) for pos, _ in loc_specs]
positions = [int(pos.rstrip("$")) for pos, _ in loc_specs]
expected = set(range(1, n + 1))
actual = set(positions)
if actual != expected:
errors.append(
f' [{language_code}] Invalid positional prefix numbers '
f'(expected 1..{n}, got {sorted(actual)}): '
f'{localized_string}'
f" [{language_code}] Invalid positional prefix numbers "
f"(expected 1..{n}, got {sorted(actual)}): "
f"{localized_string}"
)
return errors
@@ -68,29 +68,31 @@ def fix_localized_string(string_in_code, localized_string):
idx = itertools.count(1)
def replacer(m):
return f'%{next(idx)}${m.group(2)}'
return f"%{next(idx)}${m.group(2)}"
return SPECIFIER_RE.sub(replacer, localized_string)
def main():
parser = argparse.ArgumentParser(
description='Lint format specifiers in xcstrings localization files.'
description="Lint format specifiers in xcstrings localization files."
)
parser.add_argument("xcstrings_path", help="Path to the .xcstrings file")
parser.add_argument(
"--fix",
action="store_true",
help="Fix found problems and write the result back to the file",
)
parser.add_argument('xcstrings_path', help='Path to the .xcstrings file')
parser.add_argument('--fix',
action='store_true',
help='Fix found problems and write the result back to the file')
args = parser.parse_args()
xcstrings_path = Path(args.xcstrings_path)
localizable = json.loads(xcstrings_path.read_text(encoding='utf-8'))
localizable = json.loads(xcstrings_path.read_text(encoding="utf-8"))
errors_found = False
modified = False
for string_in_code, value in localizable['strings'].items():
localizations = value.get('localizations')
for string_in_code, value in localizable["strings"].items():
localizations = value.get("localizations")
if not localizations:
continue
@@ -98,15 +100,15 @@ def main():
string_errors = []
for language_code, localization_value in localizations.items():
string_unit = localization_value.get('stringUnit')
string_unit = localization_value.get("stringUnit")
if not string_unit:
continue
localized_string = string_unit.get('value', '')
errors = check_format_specifiers(string_in_code,
localized_string,
language_code)
localized_string = string_unit.get("value", "")
errors = check_format_specifiers(
string_in_code, localized_string, language_code
)
if errors:
string_errors.extend(errors)
@@ -115,23 +117,23 @@ def main():
fixed = fix_localized_string(string_in_code, localized_string)
if fixed is not None and fixed != localized_string:
string_unit['value'] = fixed
string_unit["value"] = fixed
modified = True
if string_errors:
errors_found = True
print(f'Error in {repr(string_in_code)}:')
print(f"Error in {repr(string_in_code)}:")
for error in string_errors:
print(error)
if args.fix and modified:
xcstrings_path.write_text(
json.dumps(localizable,
indent=2,
ensure_ascii=False,
separators=(',', ' : ')),
encoding='utf-8')
json.dumps(
localizable, indent=2, ensure_ascii=False, separators=(",", " : ")
),
encoding="utf-8",
)
if errors_found and not args.fix:
sys.exit(1)
+8 -9
View File
@@ -1,29 +1,28 @@
import argparse
from xml.etree import ElementTree
NS = {
'': "urn:oasis:names:tc:xliff:document:1.2"
}
NS = {"": "urn:oasis:names:tc:xliff:document:1.2"}
ElementTree.register_namespace("", "urn:oasis:names:tc:xliff:document:1.2")
ElementTree.register_namespace('', "urn:oasis:names:tc:xliff:document:1.2")
def main():
parser = argparse.ArgumentParser()
parser.add_argument('xliff')
parser.add_argument("xliff")
args = parser.parse_args()
tree = ElementTree.parse(args.xliff)
for body in tree.findall('./file/body', namespaces=NS):
for body in tree.findall("./file/body", namespaces=NS):
sorted_trans_units = []
for trans_unit in body.findall('./trans-unit', namespaces=NS):
for trans_unit in body.findall("./trans-unit", namespaces=NS):
target = trans_unit.find("./target", namespaces=NS)
if target is None:
sorted_trans_units.insert(0, trans_unit)
else:
if target.attrib.get('state') == 'translated':
if target.attrib.get("state") == "translated":
sorted_trans_units.append(trans_unit)
else:
sorted_trans_units.insert(0, trans_unit)
@@ -32,7 +31,7 @@ def main():
body.extend(sorted_trans_units)
ElementTree.indent(tree)
tree.write(args.xliff, xml_declaration=True, encoding='utf-8')
tree.write(args.xliff, xml_declaration=True, encoding="utf-8")
main()