"""Regression tests for missing files and coverage threshold boundaries. Copyright (C) 2004-2026 The Cacti Group. GPL-2.0-or-later. """ import importlib.util import hashlib import json from pathlib import Path import sys import tempfile import unittest from unittest.mock import patch import xml.etree.ElementTree as ET sys.dont_write_bytecode = True SPEC = importlib.util.spec_from_file_location( 'changed_php_coverage', Path(__file__).with_name('check_changed_php_coverage.py')) GATE = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(GATE) class ChangedPhpCoverageTest(unittest.TestCase): def test_package_evidence_requires_both_scenarios_and_current_producers(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) producers = ['tests/fixtures/Core/PackageArchive.php', 'tests/Helpers/RequestCoverage.php'] hashes = {} for name in producers: source = root / name source.parent.mkdir(parents=True, exist_ok=True) source.write_text('fixture source\n') hashes[name] = hashlib.sha256(source.read_bytes()).hexdigest() contract = {'name': 'package-archive', 'complete': True, 'assertions': 2, 'producers': producers, 'hashes': hashes} path = root / 'package-archive-test.json' GATE.validate_package_contract({'contract': contract}, path, root) for field in contract: with self.subTest(missing=field), self.assertRaises(ValueError): GATE.validate_package_contract({'contract': {k: v for k, v in contract.items() if k != field}}, path, root) for count in [0, 1, 3, True, '2']: with self.subTest(count=count), self.assertRaises(ValueError): GATE.validate_package_contract({'contract': dict(contract, assertions=count)}, path, root) with self.assertRaises(ValueError): GATE.validate_package_contract({'contract': dict(contract, complete=False)}, path, root) for name in producers: with self.subTest(missing_producer=name), self.assertRaises(ValueError): GATE.validate_package_contract({'contract': dict(contract, hashes={k: v for k, v in hashes.items() if k != name})}, path, root) (root / name).write_text('changed source\n') with self.subTest(stale=name), self.assertRaises(ValueError): GATE.validate_package_contract({'contract': contract}, path, root) (root / name).write_text('fixture source\n') def test_diff_uses_merge_base_and_ignores_deletion_only_files(self): diff = ('diff --git a/lib/a.php b/lib/a.php\n+++ b/lib/a.php\n' '@@ -4,0 +5,2 @@\n+return true;\n+}\n' 'diff --git a/lib/b.php b/lib/b.php\n+++ b/lib/b.php\n' '@@ -3 +2,0 @@\n-return false;\n') with patch.object(GATE.subprocess, 'check_output', side_effect=['merge-base\n', diff]) as command: self.assertEqual(GATE.added_lines('base', Path('.')), {'lib/a.php': {5, 6}}) self.assertIn('merge-base', command.call_args_list[1].args[0]) def test_missing_entrypoint_fails_even_if_library_is_fully_covered(self): result = {'covered': 100, 'executable': 100, 'files': { 'lib/a.php': {'covered': 100}, 'plugins.php': {'missing': True}}} self.assertFalse(GATE.passes(result, 80)) def test_threshold_does_not_round_a_failure_up_to_eighty(self): self.assertFalse(GATE.passes({'covered': 7999, 'executable': 10000, 'files': {}}, 80)) self.assertTrue(GATE.passes({'covered': 80, 'executable': 100, 'files': {}}, 80)) def test_no_executable_changes_are_not_a_missing_report(self): self.assertTrue(GATE.passes({'covered': 0, 'executable': 0, 'files': {}}, 80)) self.assertFalse(GATE.passes({'covered': 0, 'executable': 0, 'files': { 'plugins.php': {'missing': True}}}, 80)) def test_deletion_only_changes_pass_without_a_coverage_report(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) result = GATE.measure({}, root / 'coverage-changed.xml', root) self.assertEqual(result['executable'], 0) self.assertTrue(GATE.passes(result, 80)) def test_clover_counts_only_statements_on_changed_lines(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) report = root / 'coverage.xml' report.write_text(f'' '' '' '' '' '') result = GATE.measure({'plugins.php': {3, 4, 5}}, report, root) self.assertEqual(result['covered'], 1) self.assertEqual(result['executable'], 2) self.assertEqual(result['files']['plugins.php']['uncovered'], [3]) def test_config_keeps_tests_and_adds_entrypoints_to_source_scope(self): with tempfile.TemporaryDirectory() as directory: config = Path(directory) / 'input.xml' output = Path(directory) / 'output.xml' config.write_text('' 'tests/Unit' 'lib' '') GATE.prepare_config(config, output, {'lib/plugins.php': {3}, 'plugins.php': {4}}) tree = ET.parse(output) self.assertEqual(tree.getroot().attrib['bootstrap'], 'tests/bootstrap-unit.php') self.assertEqual(tree.findtext('testsuites/testsuite/directory'), 'tests/Unit') self.assertEqual([(output.parent / file.text).resolve() for file in tree.findall('source/include/file')], [(Path.cwd() / name).resolve() for name in ['lib/plugins.php', 'plugins.php']]) def test_relocated_config_keeps_bootstrap_and_test_paths(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) tests = root / 'tests' tests.mkdir() config = tests / 'phpunit.xml' output = root / 'phpunit-changed.xml' config.write_text('' '' '../tests/Unit' '') GATE.prepare_config(config, output, {'lib/plugins.php': {3}}) tree = ET.parse(output) self.assertEqual((output.parent / tree.getroot().get('bootstrap')).resolve(), (tests / 'bootstrap-unit.php').resolve()) self.assertEqual((output.parent / tree.findtext('testsuites/testsuite/directory')).resolve(), (tests / 'Unit').resolve()) def test_request_reports_require_matching_source_and_keep_uncovered_lines(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) source = root / 'plugins.php' source.write_text('' '' '' '') requests = root / 'requests' requests.mkdir() snapshot = {'root': '/var/www/html', 'coverage': { '/var/www/html/plugins.php': {'2': 1}}, 'hashes': { '/var/www/html/plugins.php': hashlib.sha256(source.read_bytes()).hexdigest()}} (requests / 'one.json').write_text(json.dumps(snapshot)) result = GATE.measure({'plugins.php': {2, 3}}, report, root, requests) self.assertEqual(result['covered'], 1) self.assertEqual(result['executable'], 2) self.assertEqual(result['files']['plugins.php']['uncovered'], [3]) source.write_text('') with self.assertRaisesRegex(ValueError, 'contains no reports'): GATE.measure({}, report, root, root) def test_bootstrap_instructions_remain_in_the_coverage_denominator(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / 'include').mkdir() source = root / 'include/global.php' source.write_text('') result = GATE.measure({'include/global.php': {2, 3}}, report, root) self.assertEqual(result['executable'], 2) self.assertFalse(GATE.passes(result, 80)) requests = root / 'requests' requests.mkdir() snapshot = {'root': '/var/www/html', 'coverage': { '/var/www/html/include/global.php': {'2': 1, '3': -1}}, 'hashes': { '/var/www/html/include/global.php': hashlib.sha256(source.read_bytes()).hexdigest()}} (requests / 'one.json').write_text(json.dumps(snapshot)) result = GATE.measure({'include/global.php': {2, 3}}, report, root, requests) self.assertEqual(result['covered'], 1) self.assertEqual(result['executable'], 2) self.assertFalse(GATE.passes(result, 80)) def test_multiline_html_is_covered_only_when_its_output_opcode_runs(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) source = root / 'page.php' source.write_text('\n
\nName\n
' '' '') requests = root / 'requests' requests.mkdir() snapshot = {'root': '/var/www/html', 'coverage': { '/var/www/html/page.php': {'4': -1}}, 'hashes': { '/var/www/html/page.php': hashlib.sha256(source.read_bytes()).hexdigest()}, 'html_spans': {'/var/www/html/page.php': [[2, 4]]}} (requests / 'one.json').write_text(json.dumps(snapshot)) self.assertEqual(GATE.measure({'page.php': {2}}, report, root, requests)['covered'], 0) snapshot['coverage']['/var/www/html/page.php']['4'] = 1 (requests / 'one.json').write_text(json.dumps(snapshot)) self.assertEqual(GATE.measure({'page.php': {2}}, report, root, requests)['covered'], 1) if __name__ == '__main__': unittest.main()