| Layer | Technology | Version |
|---|---|---|
| Language | Python | ≥ 3.11 |
| CLI framework | Typer | ≥ 0.12 |
| Terminal output | Rich | ≥ 13.7 |
| TLS probe | openssl binary | ≥ 3.0 (external, must be on PATH) |
| Testing | pytest + pytest-cov | ≥ 8 / ≥ 5 |
pip install -e ".[dev]" # editable install with dev extras
quantumvalidator check cloudflare.com # HTTPS probe
quantumvalidator check smtp.gmail.com --port 587 # SMTP/STARTTLS auto-detected
quantumvalidator check example.com --json # JSON output
quantumvalidator --version
python -m pytest # full suite with coverage
python -m pytest --tb=short -q # quick iteration
pytest tests/test_tls_utils.py -v # single modulequantumvalidator/
__init__.py Package version, NullHandler
assessor.py assess() — public API; orchestrates probe + verdict + report
checker.py check_tls() — thin probe_tls wrapper; STARTTLS auto-detected
cli.py Typer CLI: check command; --json, --output, --version flags
constants.py PQC_GROUPS, SAFE_GROUPS, PROBE_GROUPS, check_openssl()
models.py Verdict, Status, CheckResult, QuantumReport dataclasses
reporter.py print_full_report() — Rich terminal renderer (no I/O of its own)
tls_utils.py probe_tls() — subprocess openssl s_client; sole I/O boundary
verdict.py determine_verdict() / build_checks() — pure logic
tests/
conftest.py make_probe_result() factory; SAFE_PROBE / UNSAFE_* / ERROR_PROBE
test_*.py pytest, AAA pattern, class-per-feature grouping
Request lifecycle:
- CLI
checkcallsassess(target, port, timeout, progress_cb)fromassessor.py assess()callscheck_tls(host, port, timeout)fromchecker.pycheck_tls()callsprobe_tls(host, port, timeout)fromtls_utils.pyprobe_tls()reads the server's opening bytes via a plain TCP socket (1 s cap); if a STARTTLS banner is detected (smtp/imap/pop3), passes-starttls <mode>to openssl; otherwise uses raw TLS; parsesProtocol version:andNegotiated TLS1.3 group:from combined stdout+stderrassess()callsbuild_checks()anddetermine_verdict()fromverdict.py; derivesQuantumReport- Returns
QuantumReport; CLI callsprint_full_report()fromreporter.py
Single I/O boundary: all network calls go through tls_utils.probe_tls.
Two mock targets in tests:
quantumvalidator.tls_utils.subprocess.run— foropenssl s_clientcallsquantumvalidator.tls_utils.socket.create_connection— for raw banner reads (banner-first probe)
- Mock boundary:
quantumvalidator.tls_utils.subprocess.run(openssl) andquantumvalidator.tls_utils.socket.create_connection(banner read) viamonkeypatch - Fixture factory:
make_probe_result()inconftest.py— buildsTLSProbeResultwith defaults - Module-level fixtures:
SAFE_PROBE,UNSAFE_PROBE_CLASSICAL,UNSAFE_PROBE_TLS12,ERROR_PROBE - Assessor mocking: patch
"quantumvalidator.assessor.check_tls"(imported at module top) - CLI mocking: patch
"quantumvalidator.assessor.assess"and"quantumvalidator.reporter.print_full_report"(both are lazy imports insidecheck()) - Reporter tests: use
Console(file=StringIO(), no_color=True, width=200)to capture output; pass console as trailing positional arg — private helpers usecon, notconsole - Test class naming:
class TestFeatureName:, snake_case methods, AAA structure - Coverage target: 100% — enforced via
pyproject.tomladdopts
0— SAFE: PQC hybrid key exchange negotiated1— UNSAFE: classical KEX only or TLS < 1.32— Error: connection refused/timeout, DNS failure, openssl not found, invalid host/port
from __future__ import annotationsat the top of every module- Snake_case for all files, functions, and variables
- Sphinx-style docstrings:
:param name:,:returns:,:rtype:(no:type:— annotations on signatures are sufficient) - Conventional commits:
feat:,fix:,refactor:,test:,docs: assess()inassessor.pyis the sole public entry point for library usersopensslbinary is a hard external dependency; missing →RuntimeErrorraised insideprobe_tls()- OSErrors from subprocess are caught in
probe_tls()and returned asTLSProbeResult(error=...)— they never propagate to the CLI - No CI config currently present
Run these checks and update these files as needed — do not skip any step:
# 1. Verify tests pass and coverage is still 100%
pytestIf the test count changed, update both occurrences in README.md:
- Badge line (near top):
 - Running Tests section: "The test suite has NNN tests…" sentence
# 2. Verify the CLI still works
quantumvalidator --version
quantumvalidator --helpBefore pushing, update CHANGELOG.md: add your changes under ## [Unreleased]
using the standard sections (### Added, ### Changed, ### Fixed, ### Removed).
When bumping the version, move unreleased items to a new ## [x.y.z] — YYYY-MM-DD
section and update the comparison links at the bottom of CHANGELOG.md.
When committing a set of changes, bump the version using semver:
- patch (
0.1.x) — bug fixes, refactor, docs, test-only changes - minor (
0.x.0) — new PQC groups, new CLI flags, new features - major (
x.0.0) — breaking API changes
Two files must always be updated together:
pyproject.toml→version = "x.y.z"quantumvalidator/__init__.py→ fallback__version__ = "x.y.z"(theexceptbranch)
Every version bump must be followed by a GitHub release. Do not leave a version tag without a release.
After bumping the version, committing, and pushing:
# Tag the version commit and push
git tag vX.Y.Z
git push origin vX.Y.Z
# Create the GitHub release
gh release create vX.Y.Z \
--title "vX.Y.Z" \
--notes "$(cat <<'EOF'
## What's changed
<Copy the ### Added / ### Changed / ### Fixed / ### Removed blocks verbatim
from the [X.Y.Z] section in CHANGELOG.md>
## Impact
<1–3 sentences: what this means for users — what improves, what breaks,
whether the upgrade is urgent (e.g. new PQC group support, probe fix, etc.)>
## Migration
<Only for minor/major bumps: list any CLI flags, `assess()` parameters, or
output-format changes that require user action. Omit for patch releases.>
---
**Full changelog:** https://github.com/NC3-TestingPlatform/quantumvalidator/blob/master/CHANGELOG.md
EOF
)"Release body checklist:
- Changelog entries for this version copied verbatim
- Impact note written (even one sentence is enough)
- Migration note present if CLI flags or
assess()signature changed - Full changelog link at the bottom
Conventions:
- Tag and title:
vX.Y.Z— semver,v-prefixed, must matchpyproject.tomlversion - Do not mark as draft or pre-release for normal semver releases