Skip to content

Comments

Add base test coverage#37

Open
danceratopz wants to merge 6 commits intoSamWilsn:masterfrom
danceratopz:base-test-coverage
Open

Add base test coverage#37
danceratopz wants to merge 6 commits intoSamWilsn:masterfrom
danceratopz:base-test-coverage

Conversation

@danceratopz
Copy link

@danceratopz danceratopz commented Feb 23, 2026

  • Adds comprehensive test coverage to help validate planned subsequent optimizations.
  • Enable pytest-cov for coverage reports.
  • Add a developer section to the README.

- Use explicit patterns instead of recursive ** globs.
- Setuptools doesn't reliably include files with ** in sdist builds.
@danceratopz danceratopz marked this pull request as draft February 23, 2026 21:02
- 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.
- 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.
@danceratopz danceratopz marked this pull request as ready for review February 24, 2026 12:07
Copy link
Owner

@SamWilsn SamWilsn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partial review. 18 more files to go!

@@ -0,0 +1,173 @@
# Copyright (C) 2025 Ethereum Foundation
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these be 2026?

Comment on lines +31 to +34
@pytest.fixture
def temp_dir() -> Iterator[Path]:
with tempfile.TemporaryDirectory() as td:
yield Path(td)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_path: PurePath

def __init__(self, path: Optional[PurePath] = None) -> None:
self._path = path if path is not None else PurePath("mock.py")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

Comment on lines +77 to +78
settings = Settings(temp_dir, {"tool": {"docc": {}}})
plugin_settings = settings.for_plugin("test")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixture maybe?

Comment on lines +75 to +84
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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intentionally testing ConcreteDiscover, like the one defined in this file?

Comment on lines +35 to +51
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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like there are some base operations that all nodes need to implement. Would parameterization help here?

Comment on lines +105 to +134
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)}")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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>"]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then this would look something like:

Suggested change
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"]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this the default?


docc.plugins.html =
templates/**
templates/*.html
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants