mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-26 04:58:21 +09:00
* security(ci): constrain dependency installs and container build boundaries Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(docker): retain shipped runtime libraries in build context Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(tooling): quote gettext paths and honor Docker base selection Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(auth): use certificate expiry truth in rendering assertions Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(tooling): finish Docker layer and regex scan findings Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(ci): preserve documentation badge anchors without alternation Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(ci): preserve contributor badges and enforce Python coverage Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci: share changed-code coverage contracts and enforce shell policy Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(ci): enforce private review files and run path regressions Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: measure changed gettext shell coverage Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: pin coverage to the reviewed PR revision Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
84 lines
3.1 KiB
Python
84 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Require measured coverage of Python statements added by a pull request.
|
|
|
|
Copyright (C) 2004-2026 The Cacti Group. GPL-2.0-or-later.
|
|
"""
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
def added_lines(base):
|
|
base = subprocess.check_output(['git', 'merge-base', base, 'HEAD'], text=True).strip()
|
|
diff = subprocess.check_output(
|
|
['git', '-c', 'core.quotePath=false', 'diff', '--no-ext-diff', '--no-textconv',
|
|
'--unified=0', base, 'HEAD', '--', '*.py', ':(exclude)tests/**'], text=True)
|
|
changed = {}
|
|
name = None
|
|
line = 0
|
|
for record in diff.splitlines():
|
|
if record.startswith('diff --git '):
|
|
name = None
|
|
elif record.startswith('+++ b/'):
|
|
name = record[6:]
|
|
changed.setdefault(name, set())
|
|
elif record.startswith('@@'):
|
|
line = int(re.search(r'\+(\d+)', record).group(1))
|
|
elif record.startswith('+') and name is not None:
|
|
changed[name].add(line)
|
|
line += 1
|
|
elif not record.startswith(('-', '\\')):
|
|
line += 1
|
|
return {name: lines for name, lines in changed.items() if lines}
|
|
|
|
|
|
def measure(changed, report):
|
|
files = {}
|
|
for name, added in changed.items():
|
|
evidence = report['files'].get(name)
|
|
if evidence is None:
|
|
files[name] = {'missing': True}
|
|
continue
|
|
executed = set(evidence['executed_lines'])
|
|
missing = set(evidence['missing_lines'])
|
|
statements = added & (executed | missing)
|
|
files[name] = {'covered': len(statements & executed), 'executable': len(statements),
|
|
'uncovered': sorted(statements & missing)}
|
|
covered = sum(item.get('covered', 0) for item in files.values())
|
|
total = sum(item.get('executable', 0) for item in files.values())
|
|
return {'covered': covered, 'executable': total,
|
|
'percent': covered / total * 100 if total else None, 'files': files}
|
|
|
|
|
|
def passes(result, minimum):
|
|
return (not any(row.get('missing') for row in result['files'].values())
|
|
and (not result['executable'] or result['covered'] * 100 >= minimum * result['executable']))
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--base', required=True)
|
|
parser.add_argument('--report', type=Path, required=True)
|
|
parser.add_argument('--minimum', type=float, default=80)
|
|
parser.add_argument('--json-output', type=Path)
|
|
args = parser.parse_args()
|
|
if not 0 <= args.minimum <= 100:
|
|
parser.error('--minimum must be between 0 and 100')
|
|
result = measure(added_lines(args.base), json.loads(args.report.read_text()))
|
|
output = json.dumps(result, indent=2)
|
|
print(output)
|
|
if args.json_output:
|
|
args.json_output.write_text(output + '\n')
|
|
return 0 if passes(result, args.minimum) else 1
|
|
|
|
|
|
if __name__ == '__main__':
|
|
try:
|
|
sys.exit(main())
|
|
except (OSError, ValueError, KeyError, subprocess.CalledProcessError) as error:
|
|
print(f'Changed Python coverage could not be verified: {error}', file=sys.stderr)
|
|
sys.exit(1)
|