from __future__ import annotations import shutil import sys import types from pathlib import Path from typing import Any, Callable, Iterator from uuid import uuid4 import pytest PROJECT_ROOT = Path(__file__).resolve().parents[1] SRC_ROOT = PROJECT_ROOT / "src" if str(SRC_ROOT) not in sys.path: sys.path.insert(0, str(SRC_ROOT)) class FakeVisibilitySettings: _FIELD_MAP = { "show_readability": "readability", "show_analysis": "analysis", "show_paragraph_length": "paragraph_length", "show_terminology": "terminology", "show_headings": "headings", "show_structure": "structure", "show_format": "format", "show_accessibility": "accessibility", "show_document_status": "document_status", "show_acronym": "acronym", "show_prose": "prose", } def __init__(self, **kwargs: bool) -> None: for field_name in self._FIELD_MAP: setattr(self, field_name, kwargs.get(field_name, True)) def to_dict(self) -> dict[str, object]: values = { category: bool(getattr(self, field_name)) for field_name, category in self._FIELD_MAP.items() } values["version"] = "test" return values class FakeDocumentTypeRegistry: values_result = ["Advisory Circular", "Default", "Order"] @classmethod def values(cls) -> list[str]: return list(cls.values_result) class FakeContextManager: def __enter__(self) -> None: return None def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> bool: return False class FakeColumn: def __init__(self) -> None: self.metric_calls: list[tuple[str, int]] = [] self.download_calls: list[dict[str, object]] = [] def metric(self, label: str, value: int) -> None: self.metric_calls.append((label, value)) def download_button( self, label: str, *, data: bytes, file_name: str, mime: str, use_container_width: bool, ) -> bool: self.download_calls.append( { "label": label, "data": data, "file_name": file_name, "mime": mime, "use_container_width": use_container_width, } ) return True class FakeSidebar: def __init__(self) -> None: self.titles: list[str] = [] self.subheaders: list[str] = [] self.captions: list[str] = [] self.selectbox_calls: list[dict[str, object]] = [] def title(self, text: str) -> None: self.titles.append(text) def selectbox(self, label: str, *, options: list[str], index: int) -> str: self.selectbox_calls.append( {"label": label, "options": list(options), "index": index} ) return options[index] def subheader(self, text: str) -> None: self.subheaders.append(text) def checkbox(self, label: str, *, value: bool) -> bool: return value def caption(self, text: str) -> None: self.captions.append(text) class FakeStreamlitModule(types.ModuleType): def __init__(self) -> None: super().__init__("streamlit") self.sidebar = FakeSidebar() self.reset() def reset(self) -> None: self.session_state: dict[str, object] = {} self.title_calls: list[str] = [] self.caption_calls: list[str] = [] self.subheader_calls: list[str] = [] self.tabs_calls: list[list[str]] = [] self.markdown_calls: list[tuple[str, bool]] = [] self.success_calls: list[str] = [] self.error_calls: list[str] = [] self.expander_calls: list[tuple[str, bool]] = [] self.columns_history: list[list[FakeColumn]] = [] self.spinner_calls: list[str] = [] self.file_uploader_result: object | None = None self.button_result = False self.page_config_calls: list[dict[str, object]] = [] self.sidebar = FakeSidebar() def set_page_config(self, **kwargs: object) -> None: self.page_config_calls.append(kwargs) def title(self, text: str) -> None: self.title_calls.append(text) def caption(self, text: str) -> None: self.caption_calls.append(text) def file_uploader(self, *args: object, **kwargs: object) -> object | None: return self.file_uploader_result def button(self, *args: object, **kwargs: object) -> bool: return self.button_result def markdown(self, body: str, *, unsafe_allow_html: bool = False) -> None: self.markdown_calls.append((body, unsafe_allow_html)) def tabs(self, labels: list[str]) -> list[FakeContextManager]: self.tabs_calls.append(labels) return [FakeContextManager() for _ in labels] def columns(self, count: int) -> list[FakeColumn]: columns = [FakeColumn() for _ in range(count)] self.columns_history.append(columns) return columns def subheader(self, text: str) -> None: self.subheader_calls.append(text) def success(self, text: str) -> None: self.success_calls.append(text) def error(self, text: str) -> None: self.error_calls.append(text) def expander(self, label: str, *, expanded: bool = False) -> FakeContextManager: self.expander_calls.append((label, expanded)) return FakeContextManager() def spinner(self, text: str) -> FakeContextManager: self.spinner_calls.append(text) return FakeContextManager() fake_streamlit = FakeStreamlitModule() sys.modules["streamlit"] = fake_streamlit fedstylevalidator_module = types.ModuleType("fedstylevalidator") core_module = types.ModuleType("fedstylevalidator.core") models_module = types.ModuleType("fedstylevalidator.core.models") models_module.DocumentTypeRegistry = FakeDocumentTypeRegistry models_module.VisibilitySettings = FakeVisibilitySettings outputs_module = types.ModuleType("fedstylevalidator.core.outputs") def _build_canonical_json( result: dict[str, Any], *, file_path: str, doc_type: str ) -> dict[str, Any]: return { "summary": {"issue_count": 0, "issue_count_by_severity": {}}, "findings": result.get("findings", []), "metadata": {"file_path": file_path, "doc_type": doc_type}, "fun_facts": [], } def _render_output( output_format: str, result: dict[str, Any], *, file_path: str, doc_type: str ) -> str: return ( f"{output_format}:{file_path}:{doc_type}:" f"{len(result.get('findings', []))}" ) outputs_module.build_canonical_json = _build_canonical_json outputs_module.render_output = _render_output export_module = types.ModuleType("fedstylevalidator.core.export") def _save_results_as_docx(result: dict[str, Any], output_path: str) -> None: Path(output_path).write_bytes( f"docx:{len(result.get('findings', []))}".encode("utf-8") ) export_module.save_results_as_docx = _save_results_as_docx service_module = types.ModuleType("fedstylevalidator.service") docs_module = types.ModuleType("fedstylevalidator.core.docs") rules_registry_module = types.ModuleType("fedstylevalidator.core.rules.registry") def _process_document(**kwargs: object) -> dict[str, Any]: return {"by_category": {}, "findings": [], "metadata": kwargs} service_module.process_document = _process_document docs_module.render_checks_reference = lambda: ( "# Checks Reference\n\n" "## Sample Pack\n\n" "| Category | Checker | Stable rule IDs |\n" "| --- | --- | --- |\n" "| `headings` | Sample Checker | FSV001 |\n" ) docs_module.render_rules_reference = lambda rules: ( "# Rules Reference\n\n" "## FSV-HEAD-001 - Sample Rule\n\n" "**Category:** headings\n" ) rules_registry_module.RULES = [ types.SimpleNamespace( id="FSV-HEAD-001", title="Sample Rule", category="headings", description="Sample description", why_it_matters="Sample why", how_to_fix="Sample fix", examples=["Sample example"], ) ] sys.modules["fedstylevalidator"] = fedstylevalidator_module sys.modules["fedstylevalidator.core"] = core_module sys.modules["fedstylevalidator.core.docs"] = docs_module sys.modules["fedstylevalidator.core.rules.registry"] = rules_registry_module sys.modules["fedstylevalidator.core.models"] = models_module sys.modules["fedstylevalidator.core.outputs"] = outputs_module sys.modules["fedstylevalidator.core.export"] = export_module sys.modules["fedstylevalidator.service"] = service_module class UploadedFile: def __init__(self, name: str, data: bytes, *, size: int | None = None) -> None: self.name = name self._data = data if size is not None: self.size = size def getbuffer(self) -> memoryview: return memoryview(self._data) @pytest.fixture def fake_st() -> FakeStreamlitModule: fake_streamlit.reset() return fake_streamlit @pytest.fixture def uploaded_file_factory() -> Callable[..., UploadedFile]: def build( name: str = "sample.docx", data: bytes = b"test-docx-bytes", *, size: int | None = None, ) -> UploadedFile: return UploadedFile(name=name, data=data, size=size) return build @pytest.fixture def workspace_temp_dir() -> Iterator[Path]: temp_dir = PROJECT_ROOT / ".tmp-tests" / uuid4().hex temp_dir.mkdir(parents=True, exist_ok=False) try: yield temp_dir finally: shutil.rmtree(temp_dir, ignore_errors=True)