mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-16 06:23:23 +09:00
139 lines
6.3 KiB
Python
139 lines
6.3 KiB
Python
#!/usr/bin/env python3
|
|
# MobileGL - scripts/ci/lane_selection.py
|
|
# Copyright (c) 2025-2026 MobileGL-Dev
|
|
# Licensed under the GNU Lesser General Public License v3.0:
|
|
# https://www.gnu.org/licenses/gpl-3.0.txt
|
|
# https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
# SPDX-License-Identifier: LGPL-3.0-only
|
|
# End of Source File Header
|
|
|
|
"""One ctest run for several lanes: the exact names of every lane, as one --tests-from-file list.
|
|
|
|
A lane is a `ctest -L <label regex>` selection. Running the lanes of one job as one ctest
|
|
invocation lets a lane that holds a resource lock (the TCP arm runs one entry at a time) share
|
|
the run with the others. The lanes keep their own selections: each is discovered with its own
|
|
label regex, this script writes the union of the discovered names, ctest runs that list, and
|
|
junit_tally.py --select-from/--write-selected carves each lane's JUnit back out, which fails
|
|
when a lane's name is missing from the run or reported twice.
|
|
|
|
lane_selection.py union OUT.txt LANE=DISCOVERY.json [LANE=DISCOVERY.json ...]
|
|
lane_selection.py --self-test
|
|
|
|
DISCOVERY.json is `ctest -L <regex> --show-only=json-v1`. union fails when a lane selects
|
|
nothing (what --no-tests=error says for a lane run on its own), names an entry twice, or holds a
|
|
name a tests-from-file line cannot carry. It prints each lane's count and the size of the union.
|
|
"""
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
|
|
|
|
def lane_names(label, path):
|
|
"""The names one discovery selects, in order; raises ValueError when the lane is unusable."""
|
|
names = [test['name'] for test in json.loads(Path(path).read_text()).get('tests', [])]
|
|
if not names:
|
|
raise ValueError(f'lane {label}: {path} selects no test')
|
|
if len(names) != len(set(names)):
|
|
twice = sorted({name for name in names if names.count(name) > 1})
|
|
raise ValueError(f'lane {label}: {path} names {len(twice)} entr(ies) twice, e.g. {twice[0]}')
|
|
# ctest reads the list one name per line and trims each line.
|
|
bad = [name for name in names if name != name.strip() or '\n' in name or '\r' in name]
|
|
if bad:
|
|
raise ValueError(f'lane {label}: {len(bad)} name(s) cannot be written one per line, e.g. {bad[0]!r}')
|
|
return names
|
|
|
|
|
|
def union(lanes):
|
|
"""(sorted union of names, {label: count}, number of names more than one lane selects)."""
|
|
counts, seen, shared = {}, set(), set()
|
|
for label, path in lanes:
|
|
if label in counts:
|
|
raise ValueError(f'lane {label} is given twice')
|
|
names = lane_names(label, path)
|
|
counts[label] = len(names)
|
|
shared |= seen & set(names)
|
|
seen |= set(names)
|
|
return sorted(seen), counts, len(shared)
|
|
|
|
|
|
def parse_lanes(arguments):
|
|
lanes = []
|
|
for argument in arguments:
|
|
label, separator, path = argument.partition('=')
|
|
if not separator or not label or not path:
|
|
raise ValueError(f'expected LANE=DISCOVERY.json, got {argument!r}')
|
|
lanes.append((label, path))
|
|
if not lanes:
|
|
raise ValueError('no lane given')
|
|
return lanes
|
|
|
|
|
|
def run(argv):
|
|
if len(argv) < 3 or argv[0] != 'union':
|
|
print(__doc__, file=sys.stderr)
|
|
return 2
|
|
try:
|
|
names, counts, shared = union(parse_lanes(argv[2:]))
|
|
except (ValueError, OSError, json.JSONDecodeError) as error:
|
|
print(f'::error::lane_selection.py: {error}')
|
|
return 1
|
|
Path(argv[1]).write_text(''.join(name + '\n' for name in names))
|
|
summary = ', '.join(f'{label} {count}' for label, count in counts.items())
|
|
print(f'lane_selection.py: {len(names)} entr(ies) in one run ({summary}; {shared} in more than one lane)')
|
|
return 0
|
|
|
|
|
|
def self_test():
|
|
import contextlib
|
|
import io
|
|
|
|
def quiet(argv):
|
|
# The negative controls print ::error:: lines on purpose; keep them off a green run.
|
|
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
|
|
return run(argv)
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
tmp = Path(tmp)
|
|
|
|
def discovery(name, names):
|
|
path = tmp / name
|
|
path.write_text(json.dumps({'tests': [{'name': n} for n in names]}))
|
|
return str(path)
|
|
|
|
out = tmp / 'tests.txt'
|
|
split = discovery('split.json', ['DirectGLES.Split.A', 'DirectGLES.Split.B'])
|
|
spawn = discovery('spawn.json', ['DirectGLES.Spawn.A', 'SpawnLane.Control'])
|
|
tcp = discovery('tcp.json', ['TcpServer.Start', 'DirectGLES.Tcp.A', 'TcpServer.Stop'])
|
|
assert quiet(['union', str(out), f'split={split}', f'spawn={spawn}', f'tcp={tcp}']) == 0
|
|
assert out.read_text().splitlines() == sorted(
|
|
['DirectGLES.Split.A', 'DirectGLES.Split.B', 'DirectGLES.Spawn.A', 'SpawnLane.Control',
|
|
'TcpServer.Start', 'DirectGLES.Tcp.A', 'TcpServer.Stop']), 'the union is every lane, each name once'
|
|
# A name two lanes select is written once; each lane still carves its own copy.
|
|
shared = discovery('shared.json', ['DirectGLES.Split.A', 'Shared.Fixture'])
|
|
assert quiet(['union', str(out), f'split={split}', f'other={shared}']) == 0
|
|
assert out.read_text().splitlines() == ['DirectGLES.Split.A', 'DirectGLES.Split.B', 'Shared.Fixture']
|
|
out.unlink()
|
|
controls = {
|
|
'a lane that selects nothing': [f'split={split}', f"spawn={discovery('empty.json', [])}"],
|
|
'a lane that names an entry twice': [f"split={discovery('twice.json', ['A', 'A'])}"],
|
|
'a name with a trailing blank': [f"split={discovery('blank.json', ['A '])}"],
|
|
'a name with a line break': [f"split={discovery('break.json', ['A' + chr(10) + 'B'])}"],
|
|
'a lane given twice': [f'split={split}', f'split={spawn}'],
|
|
'a missing discovery file': [f"split={tmp / 'absent.json'}"],
|
|
'an argument without a lane name': [split],
|
|
}
|
|
for label, arguments in controls.items():
|
|
assert quiet(['union', str(out)] + arguments) == 1, f'{label} must fail'
|
|
assert not out.exists(), f'{label} must not leave a list behind'
|
|
assert quiet(['union', str(out)]) == 2, 'no lane is a usage error'
|
|
assert quiet(['nonsense', str(out), f'split={split}']) == 2, 'an unknown command is a usage error'
|
|
print('lane_selection self-test: ok')
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
if sys.argv[1:] == ['--self-test']:
|
|
sys.exit(self_test())
|
|
sys.exit(run(sys.argv[1:]))
|