Testing and Quality Checks
ccsinfo uses a layered quality workflow rather than one all-in-one script. tox is the repeatable test entry point, pytest runs the suite, pytest-xdist parallelizes it, ruff handles formatting and most linting, mypy enforces typing, and pre-commit bundles repository hygiene, flake8, and secret scanning.
At a Glance
toxruns the canonical automated test environment on Python 3.12.pytestdiscovers tests undertests/.pytest-xdistis enabled by default in tox via-n auto.ruffformats code and runs the primary lint pass.mypyuses strict settings for source code.flake8still runs as an additional check insidepre-commit.pre-commitalso runs secret scanners before code is committed.
Setup and Quick Start
The tox definition is intentionally small and easy to reason about:
[tox]
envlist = py312
isolated_build = true
[testenv]
allowlist_externals = uv
commands =
uv sync --extra dev
uv run pytest -n auto {posargs:tests}
The dev extra contains the core test and static-analysis tools that tox depends on:
[project.optional-dependencies]
dev = [
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"pytest-asyncio>=0.21.0",
"pytest-xdist>=3.5.0",
"ruff>=0.1.0",
"mypy>=1.5.0",
"tox>=4.0.0",
]
A practical starting point is:
uv sync --extra dev
tox
To narrow the tox run to one part of the suite, pass pytest arguments after --, for example tox -- tests/test_parsers.py.
The project metadata declares Python >=3.12, but tox currently standardizes on a single py312 environment, so that is the canonical automated test target.
Note:
pre-commitis configured in this repository, but it is not part of thedevextra shown above.flake8is also hook-managed rather than installed byuv sync --extra dev. In practice, that means tox is the built-in path for tests, whilepre-commitmust be available separately if you want hook-based runs.
Pytest
Pytest is configured in pyproject.toml rather than a separate pytest.ini:
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
addopts = ["-v", "--tb=short", "--strict-markers"]
[tool.coverage.run]
source = ["src/ccsinfo"]
branch = true
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"if __name__ == .__main__.:",
"raise NotImplementedError",
]
That configuration has a few practical consequences:
- Test discovery is limited to
tests/. - Runs are verbose by default, with shorter tracebacks.
- Unknown pytest markers are treated as errors because of
--strict-markers. - Coverage settings are ready to use, but coverage is not turned on by default by tox or by
addopts.
The checked-in suite is organized around project boundaries:
tests/test_models.pychecks Pydantic models, enums, aliases, and helper properties.tests/test_parsers.pychecks JSON and JSONL parsing, malformed input handling, and iterator helpers.tests/test_services.pycovers service-layer behavior, including patched data sources.tests/test_utils_paths.pychecks path encoding, decoding, and filesystem helpers around~/.claude.
The fixtures are designed to build realistic temporary Claude Code data layouts without touching a real home directory:
@pytest.fixture
def mock_claude_dir(
tmp_path: Path, sample_session_data: list[dict[str, Any]], sample_task_data: dict[str, Any]
) -> Path:
"""Create a fully populated mock .claude directory."""
claude_dir = tmp_path / ".claude"
# Create projects directory with a sample project
projects_dir = claude_dir / "projects"
project_dir = projects_dir / "-home-user-test-project"
project_dir.mkdir(parents=True)
# Create a session file in the project
session_file = project_dir / "abc-123-def-456.jsonl"
with session_file.open("w") as f:
for entry in sample_session_data:
f.write(json.dumps(entry) + "\n")
# Create tasks directory with a session's tasks
tasks_dir = claude_dir / "tasks"
session_tasks_dir = tasks_dir / "abc-123-def-456"
session_tasks_dir.mkdir(parents=True)
# Create a task file
task_file = session_tasks_dir / "1.json"
with task_file.open("w") as f:
json.dump(sample_task_data, f)
return claude_dir
A representative parser test shows the kind of failure handling the suite expects:
def test_parse_jsonl_skip_malformed_default(self, tmp_path: Path) -> None:
"""Test that malformed lines are skipped by default."""
content = '{"a": 1}\nnot json\n{"b": 2}'
file_path = tmp_path / "test.jsonl"
file_path.write_text(content)
results = list(parse_jsonl(file_path))
assert len(results) == 2
assert results[0]["a"] == 1
assert results[1]["b"] == 2
def test_parse_jsonl_raise_on_malformed(self, tmp_path: Path) -> None:
"""Test that malformed lines raise when skip_malformed=False."""
content = '{"a": 1}\nnot json\n{"b": 2}'
file_path = tmp_path / "test.jsonl"
file_path.write_text(content)
with pytest.raises(orjson.JSONDecodeError):
list(parse_jsonl(file_path, skip_malformed=False))
Note:
pytest-asynciois installed andasyncio_mode = "auto"is enabled, but the current checked-in suite is synchronous.Tip: The current tests are a good fit for parallel execution because they lean on
tmp_path, temporary files, and mocks instead of shared mutable global state.
Coverage
pytest-cov is part of the dev extra, and coverage settings are already in pyproject.toml, but tox does not add --cov by default. If you want coverage output, run pytest directly with pytest-cov options rather than relying on the default tox command.
Tox and xdist
tox currently defines a single environment, py312, and always runs pytest with -n auto. That means parallel test execution is the default behavior whenever you use tox.
This is the key xdist integration in the repo:
commands =
uv sync --extra dev
uv run pytest -n auto {posargs:tests}
In practice:
-n autoletspytest-xdistchoose a worker count automatically.{posargs:tests}means tox forwards extra pytest arguments.tox -- tests/test_parsers.pyis a simple way to narrow a run while keeping the same tox-managed environment.
Warning: Because tox hardcodes
-n auto, it is not the best entry point for debugging order-sensitive or timing-sensitive failures. For that kind of investigation, runpytestdirectly so you can control whether parallelism is enabled.
Ruff Formatting and Linting
Ruff is the primary formatter and linter configured in the repository:
[tool.ruff]
preview = true
line-length = 120
fix = true
output-format = "grouped"
[tool.ruff.lint]
select = ["E", "F", "W", "I", "B", "UP", "PLC0415", "ARG", "RUF059"]
[tool.ruff.format]
exclude = [".git", ".venv", ".mypy_cache", ".tox", "__pycache__"]
This setup means:
- Formatting and linting are both handled by Ruff.
- The line length target is
120. - Ruff is allowed to auto-fix many issues because
fix = trueis enabled. - Import sorting is covered by
I. - Extra rule families such as
B(bugbear),UP(pyupgrade), andARG(unused arguments) are enabled in addition to the usualE,F, andWrules.
Once the dev extra is installed, the common direct Ruff commands are:
uv run ruff format .
uv run ruff check .
Tip: Because the repo enables Ruff auto-fixes, a lint run can rewrite files. If Ruff touches a large set of files, run the tests again afterward.
mypy
Type checking is strict in this project. The main mypy settings make it clear that type annotations are part of the expected code quality bar, not an optional extra:
[tool.mypy]
check_untyped_defs = true
disallow_any_generics = true
disallow_incomplete_defs = true
disallow_untyped_defs = true
no_implicit_optional = true
show_error_codes = true
warn_unused_ignores = true
strict_equality = true
extra_checks = true
warn_unused_configs = true
warn_redundant_casts = true
Only a narrow override is applied, and it is limited to decorator-heavy CLI and router modules:
[[tool.mypy.overrides]]
module = [
"ccsinfo.cli.commands.*",
"ccsinfo.cli.main",
"ccsinfo.server.routers.*",
]
disallow_untyped_decorators = false
The source itself is written in a style that benefits from strict checking. For example, the parser layer uses generic return types instead of falling back to untyped helpers:
def parse_jsonl[T: "BaseModel"](
file_path: Path,
model: type[T] | None = None,
*,
skip_malformed: bool = True,
) -> Iterator[T | dict[str, Any]]:
If you want to run mypy directly, a typical local command is:
uv run mypy src
When mypy runs through pre-commit, it focuses on source code and brings in a few extra stub packages:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.19.1
hooks:
- id: mypy
exclude: (tests/)
additional_dependencies:
[types-requests, types-PyYAML, types-colorama, types-aiofiles]
Note: The pre-commit mypy hook excludes
tests/, so the hook is intentionally narrower than an unrestricted project-wide type check.
Flake8
Ruff is the main linter, but flake8 is still part of the repository’s quality story through pre-commit. The dedicated Flake8 config is small:
[flake8]
max-line-length = 120
extend-ignore = E203, E501, W503
exclude =
.git,
__pycache__,
.venv,
venv,
build,
dist,
*.egg-info,
And the pre-commit hook adds plugin-based checks on top of base Flake8:
- repo: https://github.com/PyCQA/flake8
rev: 7.3.0
hooks:
- id: flake8
args: [--config=.flake8]
additional_dependencies:
[git+https://github.com/RedHatQE/flake8-plugins.git, flake8-mutable]
This is important for day-to-day use:
flake8is not listed in the projectdevextra.- The supported repo-defined way to run Flake8 is through
pre-commit. - If
pre-commitreports a Flake8 failure even though Ruff is happy, the issue may be coming from the extra Flake8 plugins rather than overlapping style rules.
A focused Flake8 run looks like this:
pre-commit run flake8 --all-files
Pre-commit Hooks and Secret Scanners
pre-commit is the broadest single quality gate in the repository. It combines repository hygiene, Python quality checks, and multiple secret scanners:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-added-large-files
- id: check-docstring-first
- id: check-executables-have-shebangs
- id: check-merge-conflict
- id: check-symlinks
- id: detect-private-key
- id: mixed-line-ending
- id: debug-statements
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md] # Do not process Markdown files.
- id: end-of-file-fixer
- id: check-ast
- id: check-builtin-literals
- id: check-docstring-first
- id: check-toml
- repo: https://github.com/PyCQA/flake8
rev: 7.3.0
hooks:
- id: flake8
args: [--config=.flake8]
additional_dependencies:
[git+https://github.com/RedHatQE/flake8-plugins.git, flake8-mutable]
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.14
hooks:
- id: ruff
- id: ruff-format
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.0
hooks:
- id: gitleaks
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.19.1
hooks:
- id: mypy
exclude: (tests/)
additional_dependencies:
[types-requests, types-PyYAML, types-colorama, types-aiofiles]
In practical terms, pre-commit gives you three layers of protection:
- File hygiene checks catch common problems such as merge conflicts, trailing whitespace, missing end-of-file newlines, malformed AST/TOML, debug statements, and accidental large files.
- Python quality checks run Ruff, Flake8, and mypy together.
- Secret scanning is layered:
detect-private-keylooks for private keys,detect-secretsadds general secret detection, andgitleaksadds another credential-focused scan.
A common local workflow is:
pre-commit install
pre-commit run --all-files
Warning: Secret-detection failures should be treated seriously. If a hook flags a key, token, or other credential-like string, remove it from the change or rotate it before proceeding.
The repository also includes pre-commit.ci metadata:
ci:
autofix_prs: false
autoupdate_commit_msg: "ci: [pre-commit.ci] pre-commit autoupdate"
Note:
autofix_prs: falsemeans pre-commit.ci will not push automatic fix commits if the repository is connected to that service. Local hook runs can still rewrite files when an auto-fixing hook supports it.
CI/CD Status
No checked-in GitHub Actions, GitLab CI, or similar pipeline files are present in this repository. The only CI-related configuration found in the codebase is the ci: section in .pre-commit-config.yaml.
That means the documented quality gates in this repo are primarily local:
- Run
toxfor the project’s canonical test environment. - Run
pre-commit run --all-filesif you want the full hook suite, including Flake8 and secret scanning. - Run focused direct tools such as
uv run ruff check .oruv run mypy srcwhen you are iterating on a specific issue.
Tip: If you want a “before I commit” routine that matches the repository configuration closely,
toxpluspre-commit run --all-filesis the most complete combination available from the checked-in files.