Files
craft-common-actions/tests/test_python_coverage.py
T
skylar taylor-barrick be6bbf3ca5 Provision fonts and optional Python coverage for Sonar (#88)
* Provision coverage fonts for archive builds and test runners

* Keep coverage fonts optional unless a pin is requested

* Pin nested actions used by coverage jobs

* Export the font path only after a requested checkout

* Measure optional Python tooling coverage in Sonar

* ci: restore craft-common-actions tracking main

* ci: share Python coverage configuration with XML reporting
2026-10-25 23:37:12 -04:00

114 lines
5.5 KiB
Python

"""Exercise the optional coverage step's shell and failure propagation."""
import json
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
import yaml
ROOT = Path(__file__).resolve().parents[1]
class PythonCoverageTests(unittest.TestCase):
def setUp(self):
workflow = yaml.load((ROOT / '.github/workflows/sonarcloud.yml').read_text(), Loader=yaml.BaseLoader)
self.inputs = workflow['on']['workflow_call']['inputs']
self.steps = workflow['jobs']['sonarcloud']['steps']
self.step = next(s for s in self.steps if s.get('name') == 'Generate Python coverage')
def run_coverage(self, *, test_status=0, report_status=0, missing=False, nonexistent=False, outside=False):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = root / 'source with spaces'
source.mkdir()
script = source / 'test tools.py'
script.write_text('')
binary = root / 'bin'
binary.mkdir()
stub = binary / 'python3'
stub.write_text('''#!/usr/bin/env python3
import json, os, pathlib, shutil, sys
args = sys.argv[1:]
with open(os.environ['CALL_LOG'], 'a') as handle:
handle.write(json.dumps(args) + '\\n')
if args[:2] == ['-m', 'venv']:
target = pathlib.Path(args[2]) / 'bin' / 'python'
target.parent.mkdir(parents=True)
shutil.copyfile(__file__, target)
target.chmod(0o700)
else:
if args[:3] == ['-m', 'coverage', 'run']:
sys.exit(int(os.environ['TEST_STATUS']))
if args[:3] == ['-m', 'coverage', 'xml']:
sys.exit(int(os.environ['REPORT_STATUS']))
'''.replace('#!/usr/bin/env python3', '#!' + os.sys.executable, 1))
stub.chmod(0o700)
log = root / 'calls.jsonl'
environment = dict(os.environ, PATH=str(binary) + os.pathsep + os.environ['PATH'],
RUNNER_TEMP=str(root), CALL_LOG=str(log),
TEST_STATUS=str(test_status), REPORT_STATUS=str(report_status),
PYTHON_COVERAGE_SOURCE=str(root.parent if outside else source),
PYTHON_COVERAGE_SCRIPT='' if missing else str(source / 'absent.py' if nonexistent else script))
result = subprocess.run(['bash', '-c', self.step['run']], cwd=root,
env=environment, text=True, capture_output=True)
calls = [json.loads(line) for line in log.read_text().splitlines()] if log.exists() else []
return result, calls, str(script)
def test_paths_with_spaces_remain_single_arguments(self):
result, calls, script = self.run_coverage()
self.assertEqual(result.returncode, 0, result.stderr)
run = next(args for args in calls if args[:3] == ['-m', 'coverage', 'run'])
self.assertEqual(run[-1], script)
self.assertEqual(len([arg for arg in run if arg.startswith('--rcfile=')]), 1)
def test_failed_tests_still_generate_report_but_fail_job(self):
result, calls, _ = self.run_coverage(test_status=7)
self.assertEqual(result.returncode, 7, result.stderr)
self.assertTrue(any(args[:3] == ['-m', 'coverage', 'xml'] for args in calls))
def test_failed_report_fails_job(self):
result, _, _ = self.run_coverage(report_status=9)
self.assertEqual(result.returncode, 9, result.stderr)
def test_partial_configuration_fails_before_installing(self):
result, calls, _ = self.run_coverage(missing=True)
self.assertNotEqual(result.returncode, 0)
self.assertIn('requires both', result.stdout)
self.assertEqual(calls, [])
def test_invalid_paths_fail_before_running_tools(self):
for options in ({'nonexistent': True}, {'outside': True}):
with self.subTest(options=options):
result, calls, _ = self.run_coverage(**options)
self.assertNotEqual(result.returncode, 0)
self.assertIn('::error::', result.stdout)
self.assertEqual(calls, [])
def test_opt_in_wiring_and_report_contract(self):
for suffix in ('source', 'script'):
self.assertEqual(self.inputs['python-coverage-' + suffix]['default'], '')
self.assertEqual(self.step['env']['PYTHON_COVERAGE_' + suffix.upper()], '${{ inputs.python-coverage-' + suffix + ' }}')
self.assertEqual(self.step['if'], "steps.token.outputs.available == 'true' && (inputs.python-coverage-source != '' || inputs.python-coverage-script != '')")
result, calls, script = self.run_coverage()
self.assertEqual(result.returncode, 0, result.stderr)
run = next(args for args in calls if args[:3] == ['-m', 'coverage', 'run'])
report = next(args for args in calls if args[:3] == ['-m', 'coverage', 'xml'])
config = next(arg for arg in run if arg.startswith('--rcfile='))
self.assertNotEqual(config, '--rcfile=/dev/null')
self.assertEqual(report, ['-m', 'coverage', 'xml', config, '-o', 'python-coverage.xml'])
self.assertIn('source = ${PYTHON_COVERAGE_SOURCE_REAL}', self.step['run'])
self.assertIn('omit = ${PYTHON_COVERAGE_SCRIPT_REAL}', self.step['run'])
def test_failure_is_not_suppressed_and_step_precedes_scan(self):
self.assertNotIn('continue-on-error', self.step)
scan = next(s for s in self.steps if s.get('name') == 'SonarCloud Scan')
self.assertLess(self.steps.index(self.step), self.steps.index(scan))
if __name__ == '__main__':
unittest.main()