Files
cacti/tests/tools/test_check_changed_php_coverage.py
Thomas Vincent 6254b2c17d fix(package): name template archives without export-form fields (#8431)
* fix(packaging): name archives from supplied metadata

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(packaging): make archive leaf-name confinement explicit

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test(e2e): move the pointer off the Deepness multiselect button

Deepness sets a zero body margin, so the unhovered #ms-button sits at the viewport origin and headless Chromium on Linux intermittently applies :hover to it.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* ci: pull pinned integration database from authenticated mirror

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-23 10:37:58 -06:00

209 lines
12 KiB
Python

"""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'<coverage><project><file name="{root}/plugins.php">'
'<line num="3" type="method" count="1"/>'
'<line num="3" type="stmt" count="0"/>'
'<line num="4" type="stmt" count="2"/>'
'<line num="9" type="stmt" count="5"/>'
'</file></project></coverage>')
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('<phpunit bootstrap="tests/bootstrap-unit.php">'
'<testsuites><testsuite name="Unit"><directory>tests/Unit</directory>'
'</testsuite></testsuites><source><include><directory>lib</directory>'
'</include></source></phpunit>')
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('<phpunit bootstrap="../tests/bootstrap-unit.php">'
'<testsuites><testsuite name="Unit">'
'<directory>../tests/Unit</directory>'
'</testsuite></testsuites></phpunit>')
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('<?php\nreturn true;\n')
report = root / 'coverage.xml'
report.write_text(f'<coverage><project><file name="{source}">'
'<line num="2" type="stmt" count="0"/>'
'<line num="3" type="stmt" count="0"/>'
'</file></project></coverage>')
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('<?php\nreturn false;\n')
with self.assertRaisesRegex(ValueError, 'source does not match'):
GATE.measure({'plugins.php': {2, 3}}, report, root, requests)
def test_missing_request_reports_do_not_pass(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
report = root / 'coverage.xml'
report.write_text('<coverage/>')
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('<?php\n$theme = "modern";\n$_SESSION["theme"] = $theme;\n')
report = root / 'coverage.xml'
report.write_text(f'<coverage><project><file name="{source}"/></project></coverage>')
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('<?php ?>\n<header>\nName\n</header><?php return;\n')
report = root / 'coverage.xml'
report.write_text(f'<coverage><project><file name="{source}">'
'<line num="2" type="stmt" count="0"/>'
'</file></project></coverage>')
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()