Conversation
- Use explicit patterns instead of recursive ** globs. - Setuptools doesn't reliably include files with ** in sdist builds.
- Add dedicated test extras with pytest and pytest-cov.
- Add test extras to both test and type environments so pytest and pytest-cov are available alongside lint deps.
- Configure pytest to run with coverage enabled. - Set minimum coverage threshold to 80%. - Exclude common non-testable patterns from coverage.
16d08cb to
df68bf1
Compare
- Add tests for CLI, settings, context, and document modules. - Add tests for plugin loader and transform pipeline. - Add tests for HTML, mistletoe, and verbatim plugins. - Add end-to-end HTML rendering pipeline tests. - Add tests for Python CST parsing and node types. - Add tests for references, search, and resources plugins. - Add integration tests for end-to-end workflows. - Add behavior-level pipeline contract tests. - Achieve 90% code coverage.
df68bf1 to
17b9061
Compare
SamWilsn
left a comment
There was a problem hiding this comment.
Partial review. 18 more files to go!
| @@ -0,0 +1,173 @@ | |||
| # Copyright (C) 2025 Ethereum Foundation | |||
| @pytest.fixture | ||
| def temp_dir() -> Iterator[Path]: | ||
| with tempfile.TemporaryDirectory() as td: | ||
| yield Path(td) |
There was a problem hiding this comment.
Would https://docs.pytest.org/en/stable/how-to/tmp_path.html be useful here?
| _path: PurePath | ||
|
|
||
| def __init__(self, path: Optional[PurePath] = None) -> None: | ||
| self._path = path if path is not None else PurePath("mock.py") |
There was a problem hiding this comment.
| self._path = path if path is not None else PurePath("mock.py") | |
| self._path = PurePath("mock.py") if path is None else path |
The negation adds cognitive load, IMO
| settings = Settings(temp_dir, {"tool": {"docc": {}}}) | ||
| plugin_settings = settings.for_plugin("test") |
| class TestDiscover: | ||
| def test_concrete_discover(self, temp_dir: Path) -> None: | ||
| settings = Settings(temp_dir, {"tool": {"docc": {}}}) | ||
| plugin_settings = settings.for_plugin("test") | ||
|
|
||
| discover = ConcreteDiscover(plugin_settings) | ||
| sources = list(discover.discover(frozenset())) | ||
|
|
||
| assert len(sources) == 1 | ||
| assert isinstance(sources[0], MockSource) |
There was a problem hiding this comment.
Is this intentionally testing ConcreteDiscover, like the one defined in this file?
| class TestBlankNode: | ||
| def test_children_returns_empty_tuple(self) -> None: | ||
| node = BlankNode() | ||
| assert tuple(node.children) == () | ||
|
|
||
| def test_replace_child_raises_type_error(self) -> None: | ||
| node = BlankNode() | ||
| with pytest.raises(TypeError): | ||
| node.replace_child(BlankNode(), BlankNode()) | ||
|
|
||
| def test_repr(self) -> None: | ||
| node = BlankNode() | ||
| assert repr(node) == "<blank>" | ||
|
|
||
| def test_bool_is_false(self) -> None: | ||
| node = BlankNode() | ||
| assert bool(node) is False |
There was a problem hiding this comment.
I feel like there are some base operations that all nodes need to implement. Would parameterization help here?
| class RecordingVisitor(Visitor): | ||
| events: List[str] | ||
|
|
||
| def __init__(self) -> None: | ||
| self.events = [] | ||
|
|
||
| @override | ||
| def enter(self, node: Node) -> Visit: | ||
| self.events.append(f"enter:{repr(node)}") | ||
| return Visit.TraverseChildren | ||
|
|
||
| @override | ||
| def exit(self, node: Node) -> None: | ||
| self.events.append(f"exit:{repr(node)}") | ||
|
|
||
|
|
||
| class SkippingVisitor(Visitor): | ||
| events: List[str] | ||
|
|
||
| def __init__(self) -> None: | ||
| self.events = [] | ||
|
|
||
| @override | ||
| def enter(self, node: Node) -> Visit: | ||
| self.events.append(f"enter:{repr(node)}") | ||
| return Visit.SkipChildren | ||
|
|
||
| @override | ||
| def exit(self, node: Node) -> None: | ||
| self.events.append(f"exit:{repr(node)}") |
There was a problem hiding this comment.
| class RecordingVisitor(Visitor): | |
| events: List[str] | |
| def __init__(self) -> None: | |
| self.events = [] | |
| @override | |
| def enter(self, node: Node) -> Visit: | |
| self.events.append(f"enter:{repr(node)}") | |
| return Visit.TraverseChildren | |
| @override | |
| def exit(self, node: Node) -> None: | |
| self.events.append(f"exit:{repr(node)}") | |
| class SkippingVisitor(Visitor): | |
| events: List[str] | |
| def __init__(self) -> None: | |
| self.events = [] | |
| @override | |
| def enter(self, node: Node) -> Visit: | |
| self.events.append(f"enter:{repr(node)}") | |
| return Visit.SkipChildren | |
| @override | |
| def exit(self, node: Node) -> None: | |
| self.events.append(f"exit:{repr(node)}") | |
| class RecordingVisitor(Visitor): | |
| returns: ClassVar[Visit] = Visit.TraverseChildren | |
| events: List[Tuple[Literal["enter", "exit"], int]] | |
| def __init__(self) -> None: | |
| self.events = [] | |
| @override | |
| def enter(self, node: Node) -> Visit: | |
| self.events.append(("enter", id(node))) | |
| return self.returns | |
| @override | |
| def exit(self, node: Node) -> None: | |
| self.events.append(("exit", id(node))) | |
| class SkippingVisitor(RecordingVisitor): | |
| returns: ClassVar[Visit] = Visit.SkipChildren |
Two major suggestions: use id instead of repr because you can check object identity, and inherit instead of re-implementing.
Untested!
| visitor = RecordingVisitor() | ||
| node.visit(visitor) | ||
|
|
||
| assert visitor.events == ["enter:<blank>", "exit:<blank>"] |
There was a problem hiding this comment.
Then this would look something like:
| assert visitor.events == ["enter:<blank>", "exit:<blank>"] | |
| assert visitor.events == [ | |
| ("enter", id(node)), | |
| ("exit", id(node)), | |
| ] |
| [tool.pytest.ini_options] | ||
| # term-missing shows uncovered line numbers in terminal output | ||
| addopts = "--cov=docc --cov-report=term-missing" | ||
| testpaths = ["tests"] |
|
|
||
| docc.plugins.html = | ||
| templates/** | ||
| templates/*.html |
Uh oh!
There was an error while loading. Please reload this page.