Thanks for your interest. This project is governed by the Master Tester Doctrine. Read it before writing tests. The 10 Laws are non-negotiable.
- Doctrine — must-read before writing tests
- Architecture — module responsibilities
- Migration guide for v1.0.x users
- Security policy + reporting channel
- Troubleshooting decision tree
Be respectful. Be constructive. Be patient. Disagreements about technical decisions are healthy; disagreements about people are not.
Reports of unacceptable conduct can be sent privately via the
channels in SECURITY.md.
| Tool | Why | Install |
|---|---|---|
python 3.10+ |
Runtime | system package or pyenv |
adb |
What we wrap | pkg install android-tools (Termux) / brew install --cask android-platform-tools / apt install adb |
git |
Source control | system package |
pre-commit |
Local hook runner | pip install pre-commit |
gh repo fork hah23255/adb-android-control --clone
cd adb-android-control
pip install -e ".[dev]"
pre-commit installVerify everything works:
ruff check .
mypy adb_android_control
pytest -qAll three should pass on a fresh clone.
This project follows the Master Tester Doctrine end-to-end. Before your PR can land, it must align with the 10 Laws + Patterns. The most common pitfalls:
| Pitfall | Doctrine Law | What to do instead |
|---|---|---|
| Modifying a test to make CI pass | Law 1 | Fix the production code. If the test is genuinely wrong, open a separate PR explaining why with reviewer sign-off — never combine with a "make CI green" change. |
Asserting obj._private_field == ... |
Law 2 | Test the public method that observes the private field's effect. |
| One test per dataclass field | Law 9 | One test per behaviour. A factory test is one concept; a getter test isn't usually a concept. |
subprocess.run mocked directly |
Law 6 | Use the mock_adb Poison-Pill fixture from tests/conftest.py. |
# type: ignore in a test |
Law 7 | Reshape the type to be testable, or use cast(SomeRealType, ...) with rationale. The only allowed exception is # noqa: B017 for frozen-dataclass with pytest.raises(Exception): patterns. |
time.sleep(0.1) in a test |
Law 8 | Use freezegun or inject the clock via the production code's _now/now_fn indirection. |
- Branch.
git checkout -b feature/your-thing - Read the relevant doctrine sections. Especially: the 10 Laws and project entrypoint, the lessons section in that file (Adaptive Fault Tolerance is the one you'll engage most often).
- Write the failing test first. Place it under the right
directory:
tests/unit/— default; mocks everythingtests/property/— Hypothesis property-basedtests/race/— threading + concurrencytests/integration/— needs realadb(skipped by default)tests/quarantine/— known-flaky (must have an open issue link)
- Implement the production code until the test passes.
- Run the full suite.
pytest -q— including property + race. - Run the linters.
ruff check .,mypy adb_android_control,pre-commit run --all-files. - Commit following Conventional Commits (see below).
- Push and open a PR with the PR template filled in (forthcoming in Phase 7).
- Strict mypy. No
Anyannotations (disallow_any_explicit = true). No# type: ignorein production code; rare exceptions in tests are flagged by reviewers. - Ruff for lint + format. Trailing whitespace, import order, pep8-naming, flake8-bugbear, flake8-bandit, flake8-use-pathlib, pyupgrade, etc. — all enforced.
- Frozen dataclasses for value objects. Mutable state lives in classes with explicit lifecycles.
- Pure functions for parsers, converters, predicates. They live at module level so tests can target them directly (Law 9).
- Dependency injection for I/O. Class constructors accept
adb=ADBController(),notifier=...,now_fn=..., etc., so tests can substitute fakes (Laws 5 + 6). - No
shell=True. Argv lists only.subprocesscalls usecheck=Falseand explicit error mapping. - No bare
except:. Use specific exception classes. Where you must catch broadly for graceful degradation (internal lesson (adaptive fault tolerance)), comment the rationale inline. - Library code does NOT call
logging.basicConfig. That's the application's concern; we install logging only incli.py.
We use Conventional Commits. Format:
<type>(<scope>): <subject>
<body — wrap at 72 chars; explain *why*, not what>
<optional footer — Co-Authored-By, Closes #N, etc.>
Types: feat, fix, refactor, test, docs, chore,
security, perf, revert, ci, build.
Scopes: module names without _ prefixes — controller,
monitor, automation, radio, connection_monitor, port_scan,
usb, cli, tests, docs, ci. For multi-scope changes use
comma,separated or <scope>+.
test(controller): add Hypothesis property fuzzing on _BATTERY_LEVEL_RE
Adds three property tests covering the regex's never-raises invariant
plus the well-formed-line round-trip identity. Closes the v1.1.0-rc1
property-coverage gap surfaced in code review.
Co-Authored-By: …
fix(monitor): handle EOF mid-line in LogcatMonitor._read_loop
Reproduces deterministically with a SIGTERM mid-readline injection.
The pre-fix path raised AttributeError on the truncated stdout.
Closes #42
- ❌ Don't combine refactors with bug fixes in one commit.
- ❌ Don't include "WIP" or "fix typo" commits in PRs — squash before merge.
- ❌ Don't skip the body unless the change is genuinely trivial.
- Open a draft PR early; you don't have to wait until it's green.
- The PR template asks specifically for:
- Which Doctrine Laws the change engages
- Which test directory the new tests live in
- Whether any test files were modified (Law 1 gate)
- CI runs lint, typecheck, tests, coverage delta — all must pass.
- At least one reviewer must approve. For doctrine-violation exceptions (e.g., a deliberately weak test for a known-broken subsystem), maintainer sign-off required.
- Squash-merge is the default. Maintainer may request a rebase merge for multi-commit feature branches with clean history.
If your change adds public API, update:
- The relevant module docstring
docs/ARCHITECTURE.mdif architecturaldocs/USE_CASES.mdwith a new UC entry if user-visibleCHANGELOG.mdunder[Unreleased]
For docs-only changes, no test changes are required, but markdownlint
runs in pre-commit.
See docs/TESTING_DOCTRINE.md for the
full doctrine. Quick reference:
pytest # all unit + property + race (default)
pytest -m unit # unit only
pytest -m property # Hypothesis property tests
pytest -m race # threading / concurrency
pytest -m integration # real adb (skipped by default; opt-in)
pytest --cov # coverage report
pytest --cov --cov-fail-under=80 # gate at 80% (target — Phase 3 done)
pytest -x -q # fail fast, quiet output
pytest tests/unit/test_controller.py::TestPackages # one classReviewers look for, in order:
- Doctrine compliance. Especially Laws 1, 6, 7, 8.
- Test coverage. Every public method has at least one test.
- Type safety.
mypy --strictmust pass. - Style.
ruff check .clean. - Documentation. Public API has docstrings; CHANGELOG updated.
- Architectural fit. No new circular deps; layering rules respected.
A PR that fails (1) is rejected; (2)–(6) are negotiable with reviewer sign-off.
Releases are cut by maintainers from main after a green CI matrix.
Versioning follows SemVer. Releases are tagged
and published via GitHub Releases (PyPI in Phase 7).
Phase 7 will introduce a Developer Certificate of Origin sign-off requirement on commits. After that lands, every commit needs:
Signed-off-by: Your Name <your.email@example.com>
Run git commit -s to add this automatically. Pre-existing
contributions are grandfathered.