yams/tests/scripts/test_check_portability.py

167 lines
6.2 KiB
Python

#!/usr/bin/env python3
"""Unit tests for the cross-platform portability policy scanner."""
from __future__ import annotations
import importlib.util
import sys
import tempfile
import unittest
from pathlib import Path
SCRIPT = Path(__file__).with_name("check_portability.py")
SPEC = importlib.util.spec_from_file_location("check_portability", SCRIPT)
assert SPEC and SPEC.loader
POLICY = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = POLICY
SPEC.loader.exec_module(POLICY)
class PortabilityPolicyTests(unittest.TestCase):
def scan_source(self, source: str, suffix: str = ".cpp"):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
path = root / "tests" / f"sample{suffix}"
path.parent.mkdir(parents=True)
path.write_text(source, encoding="utf-8")
return POLICY.scan(root, ("tests",))
def test_detects_all_three_rule_classes(self):
findings = self.scan_source(
"""
void f(std::filesystem::path p) {
std::jthread worker{[] {}};
sqlite3_open(p.c_str(), nullptr);
const char* v = std::getenv("YAMS_X");
}
"""
)
self.assertEqual(
{finding.rule for finding in findings},
{"jthread", "path-cstr", "env-read"},
)
def test_masks_comments_literals_and_boundary_helpers(self):
findings = self.scan_source(
r"""
// std::jthread worker;
/* std::getenv("YAMS_X"); */
const char* raw = "sqlite3_open(path.c_str())";
auto a = yams::config::getenv_optional("YAMS_X");
auto b = yams::config::getenv_nonempty("YAMS_X");
"""
)
self.assertEqual(findings, [])
def test_digit_separators_do_not_hide_later_violations(self):
findings = self.scan_source(
"""
void f() {
auto big = 1'000'000;
std::jthread worker{[] {}};
std::getenv("YAMS_X");
}
"""
)
self.assertEqual(
{finding.rule for finding in findings},
{"jthread", "env-read"},
)
def test_path_cstr_detects_member_and_call_forms(self):
findings = self.scan_source(
"""
void f() {
sqlite3_open(obj.path.c_str(), nullptr);
sqlite3_open(getPath().c_str(), nullptr);
sqlite3_open((path).c_str(), nullptr);
}
"""
)
self.assertEqual(
{finding.rule for finding in findings},
{"path-cstr"},
)
self.assertEqual(len(findings), 3)
def test_path_cstr_skips_string_conversion_but_flags_mixed_ternary(self):
findings = self.scan_source(
"""
void f() {
sqlite3_open(path.string().c_str(), nullptr);
sqlite3_open(flag ? safe.string().c_str() : path.c_str(), nullptr);
}
"""
)
self.assertEqual(
{finding.rule for finding in findings},
{"path-cstr"},
)
self.assertEqual(len(findings), 1)
def test_detects_analyzer_only_shims(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
fixtures = {
"include/yams/compat/core_types.h": "template <typename T> class Result {};\n",
"include/yams/compat/optional.h": "template <typename T> class Optional {};\n",
"include/yams/compat/catch2_test_macros.h": "#define REQUIRE(...) ((void)0)\n",
"include/yams/compat/boost_asio_core.h": (
'#include "/opt/homebrew/opt/boost/include/boost/asio/awaitable.hpp"\n'
),
"yams/core/types.h": '#include "../../include/yams/core/types.h"\n',
}
for relative, content in fixtures.items():
path = root / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
findings = POLICY.scan(root, ("include/yams", "yams"))
self.assertEqual(
{finding.rule for finding in findings},
{
"analyzer-result-shim",
"analyzer-optional-shim",
"catch2-noop-shim",
"absolute-include",
"shadow-public-header",
},
)
def test_exempts_only_central_boundary(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
boundary = root / POLICY.CENTRAL_BOUNDARY
boundary.parent.mkdir(parents=True)
boundary.write_text('void f() { std::getenv("YAMS_X"); }', encoding="utf-8")
self.assertEqual(POLICY.scan(root, ("src",)), [])
def test_allowlist_requires_reasons_and_stale_entries_are_observable(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = root / "tests" / "sample.cpp"
source.parent.mkdir(parents=True)
source.write_text("void f() { std::jthread w{[] {}}; }", encoding="utf-8")
finding = POLICY.scan(root, ("tests",))[0]
allowlist_path = root / "allowlist.txt"
allowlist_path.write_text(
f"{finding.path}:{finding.line}:portability/{finding.rule} # reviewed\n",
encoding="utf-8",
)
allowlist = POLICY.load_allowlist(allowlist_path)
self.assertIn(finding.allowlist_key, allowlist)
source.write_text("void f() {}\n", encoding="utf-8")
live_keys = {item.allowlist_key for item in POLICY.scan(root, ("tests",))}
self.assertEqual(set(allowlist) - live_keys, {finding.allowlist_key})
allowlist_path.write_text(
f"{finding.path}:{finding.line}:portability/{finding.rule}\n",
encoding="utf-8",
)
with self.assertRaises(ValueError):
POLICY.load_allowlist(allowlist_path)
if __name__ == "__main__":
unittest.main()