diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..4b00352 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,87 @@ +#!/bin/bash + +set -e + +repo_root=$(git rev-parse --show-toplevel) +cd "$repo_root" + +if [ ! -f requirements.txt ]; then + echo "Error: requirements.txt is missing from the repository root." + exit 1 +fi + +echo "Checking code style with Black..." +make format-check + +if [ -n "${TEST_FILES:-}" ]; then + echo "Running selected tests from TEST_FILES..." + make test-fast TEST_FILES="$TEST_FILES" +elif [ "${TEST_SCOPE:-fast}" = "full" ]; then + echo "Running the full test and coverage gate..." + make test-full +else + staged_tests=() + unknown_python_change=0 + + while IFS= read -r path; do + case "$path" in + tests/test_*.py) + staged_tests+=("$path") + ;; + src/compression/*) + staged_tests+=("tests/test_compression.py") + ;; + src/data_structures/*) + staged_tests+=("tests/test_data_structures.py") + staged_tests+=("tests/test_service_tools.py" "tests/test_service_mcp_server.py" "tests/test_service_http_app.py") + ;; + src/dynamic_programming/*) + staged_tests+=("tests/test_dynamic_programming.py") + staged_tests+=("tests/test_service_tools.py" "tests/test_service_mcp_server.py" "tests/test_service_http_app.py") + ;; + src/graphs/*) + staged_tests+=("tests/test_graphs.py") + staged_tests+=("tests/test_service_tools.py" "tests/test_service_mcp_server.py" "tests/test_service_http_app.py") + ;; + src/machine_learning/*) + staged_tests+=("tests/test_machine_learning.py") + staged_tests+=("tests/test_service_tools.py" "tests/test_service_mcp_server.py" "tests/test_service_http_app.py") + ;; + src/numeric/*) + staged_tests+=("tests/test_numeric.py") + staged_tests+=("tests/test_service_tools.py" "tests/test_service_mcp_server.py" "tests/test_service_http_app.py") + ;; + src/searching/*) + staged_tests+=("tests/test_searching.py") + staged_tests+=("tests/test_service_tools.py" "tests/test_service_mcp_server.py" "tests/test_service_http_app.py") + ;; + src/sorting/*) + staged_tests+=("tests/test_sorting.py") + staged_tests+=("tests/test_service_tools.py" "tests/test_service_mcp_server.py" "tests/test_service_http_app.py") + ;; + src/string_matching/*) + staged_tests+=("tests/test_string_matching.py") + staged_tests+=("tests/test_service_tools.py" "tests/test_service_mcp_server.py" "tests/test_service_http_app.py") + ;; + service/*) + staged_tests+=("tests/test_service_tools.py" "tests/test_service_mcp_server.py" "tests/test_service_http_app.py") + ;; + *.py) + unknown_python_change=1 + ;; + esac + done < <(git diff --cached --name-only --diff-filter=ACMR) + + if [ "$unknown_python_change" -eq 1 ]; then + echo "Running the full test suite for an unmapped Python change..." + make test-full + elif [ "${#staged_tests[@]}" -eq 0 ]; then + echo "No staged Python changes; skipping tests." + else + unique_tests=($(printf '%s\n' "${staged_tests[@]}" | sort -u)) + echo "Running staged-area tests: ${unique_tests[*]}" + make test-fast TEST_FILES="${unique_tests[*]}" + fi +fi + +echo "Pre-commit checks passed." diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2daea0..314d37d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,14 +34,9 @@ jobs: - name: Execute Automated Test Suite with Coverage Tracking run: | - # Generates terminal outputs, an interactive HTML site, and an XML map - # Enforces a strict minimum test coverage baseline barrier of 100% - pytest tests/ \ - --cov=src \ - --cov-fail-under=100 \ - --cov-report=term-missing \ - --cov-report=html:htmlcov \ - --cov-report=xml:coverage.xml + # Enforces the shared 100% coverage gate used by full local verification. + make test-full \ + PYTEST="pytest --cov-report=html:htmlcov --cov-report=xml:coverage.xml" # Packages the generated interactive HTML site as a downloadable dashboard - name: Upload Core Coverage HTML Dashboard diff --git a/.gitignore b/.gitignore index 5fd6222..9c80806 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,18 @@ venv/ .venv/ .pytest_cache/ .coverage +coverage.xml +htmlcov/ +\.env +\.env.* +!\.env.example +*.pem +*.key +*.p12 +*.pfx +*.secret +*.secrets +*.log +\.DS_Store *.txt !requirements.txt diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cc7697b --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +PYTEST ?= pytest +BLACK ?= black + +.PHONY: format-check test-fast test-full verify + +format-check: + $(BLACK) --check src/ tests/ service/ + +test-fast: + $(PYTEST) $(TEST_FILES) + +test-full: + $(PYTEST) tests/ --cov=src --cov=service --cov-fail-under=100 --cov-report=term-missing + +verify: format-check test-full diff --git a/README.md b/README.md index 2f77afa..aa72645 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,8 @@ To run syntax and style validation checks using `flake8` or `black`: black --check src/ tests/ ``` +For the full test and coverage gate, run `make verify`. Local commits use the tracked `.githooks/pre-commit` hook for fast staged-area tests by default; use `TEST_SCOPE=full git commit` to run the complete 100% coverage gate before committing. + ## Running the Service Layer Every algorithm is also exposed via a stateless MCP server and a REST API, both backed by the same `service/tools.py` wrapper functions. diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index fd8c5bc..4d779e4 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -13,6 +13,8 @@ source .venv/bin/activate pip install -r requirements.txt ``` +Do not commit credentials, local environment files, private keys, coverage reports, or logs. The repository ignores common forms of these files; review `git status` and `git diff --cached` before pushing, and use environment variables for any local secrets. + `.venv/` is self-ignored (it ships its own `.gitignore`), so it never needs to be added to the repo's ignore rules. Use `.venv/bin/pytest`, `.venv/bin/black`, etc. if the venv isn't activated in your shell. > **Two Python environments exist.** The local pre-commit hook (see Section 4) runs `pytest`/`black` from a separate, globally-installed interpreter — not `.venv`. If you add a new dependency (e.g. to `service/`), install it in **both** places, or the hook will fail with `ModuleNotFoundError` at commit time: @@ -43,14 +45,27 @@ Each new algorithm addition should include, as applicable: ## 4. Formatting & Local Verification -Run these before committing — a pre-commit hook enforces them anyway, but running locally first avoids failed commits: +The tracked `.githooks/pre-commit` hook runs Black plus tests related to staged Python files by default. Install it for a clone with: + +```bash +git config core.hooksPath .githooks +``` + +Use the fast default for normal commits. To run specific tests: + +```bash +TEST_FILES="tests/test_graphs.py" git commit +``` + +To run the complete repository gate locally: ```bash -.venv/bin/black src/ tests/ service/ -.venv/bin/pytest tests/ --cov=src --cov=service --cov-report=term-missing +TEST_SCOPE=full git commit +# or, without committing: +make verify ``` -Confirm the coverage report shows 100% for all modified/added files. +The full gate enforces 100% coverage across `src` and `service`; selective tests are intended for quick local feedback, while CI always runs the full gate. ## 5. Committing @@ -107,8 +122,9 @@ git fetch origin --prune # clears stale remote-tracking |---|---| | Sync main | `git checkout main && git pull origin main` | | New branch | `git checkout -b feature/` | -| Format | `.venv/bin/black src/ tests/ service/` | -| Test + coverage | `.venv/bin/pytest tests/ --cov=src --cov=service --cov-report=term-missing` | +| Install hooks | `git config core.hooksPath .githooks` | +| Fast selected tests | `TEST_FILES="tests/test_graphs.py" git commit` | +| Full test + coverage | `TEST_SCOPE=full git commit` or `make verify` | | Push | `git push -u origin ` | | Open PR | `gh pr create --title "..." --body "..." --base main --head ` | | Merge PR | `gh pr merge --squash --delete-branch` |