fedstylevalidator / tests /conftest.py
Eric Putnam
chore: sync fsv 0.1.1 deployment
40f278f
Raw History Blame Contribute Delete
9.59 kB
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"<html><body>{output_format}:{file_path}:{doc_type}:"
f"{len(result.get('findings', []))}</body></html>"
)
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)