diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f612a5..5a69797 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,112 +1,141 @@ -name: CI +# =============================================================== +# ๐Ÿงช Tests & Coverage + ๐Ÿ“ฆ Package Build +# =============================================================== +# +# This workflow: +# - Runs pytest with 85% coverage threshold (test) +# - Uploads coverage.xml as artifact (test) +# - Builds wheel/sdist and validates with twine (package) +# +# Lint, type-check, and pre-commit live in separate workflows +# (lint.yml, pre-commit.yml) so they appear as independent +# checks on PRs. +# +# Triggers on push to main and PRs. Skips draft PRs. +# Cancels stale runs on the same branch. +# +# References +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# - Poetry: https://python-poetry.org/docs/ +# - Pytest: https://docs.pytest.org/ +# - Twine: https://twine.readthedocs.io/ +# +# Author: Manav Gupta +# =============================================================== + +name: Tests & Package on: push: branches: [main] pull_request: + types: [opened, synchronize, ready_for_review] branches: [main] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +# ----------------------------------------------------------------- +# Minimal permissions โ€” principle of least privilege +# ----------------------------------------------------------------- permissions: contents: read +# ----------------------------------------------------------------- +# Shared environment +# ----------------------------------------------------------------- +env: + PYTHONUNBUFFERED: "1" + PIP_DISABLE_PIP_VERSION_CHECK: "1" + jobs: - lint: - name: Lint + + # ========================================================================= + # ๐Ÿงช Tests & Coverage + # ========================================================================= + test: + name: Test (py${{ matrix.python }}) runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 10 if: github.event_name != 'pull_request' || !github.event.pull_request.draft - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - - uses: actions/setup-python@v5 - with: - python-version: "3.12" + strategy: + fail-fast: false + matrix: + python: ["3.12"] - - name: Install Poetry - run: pipx install poetry - - - name: Install dependencies - run: poetry install --no-interaction - - - name: Ruff check - run: poetry run ruff check . - - - name: Black check - run: poetry run black --check . - - typecheck: - name: Type Check - runs-on: ubuntu-latest - timeout-minutes: 5 - if: github.event_name != 'pull_request' || !github.event.pull_request.draft steps: - - uses: actions/checkout@v4 + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Setup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + - name: โฌ‡๏ธ Checkout code + uses: actions/checkout@v4 with: persist-credentials: false - - uses: actions/setup-python@v5 + - name: ๐Ÿ Setup Python ${{ matrix.python }} + uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: ${{ matrix.python }} - - name: Install Poetry + - name: ๐Ÿ“ฆ Install Poetry run: pipx install poetry - - name: Install dependencies - run: poetry install --no-interaction - - - name: Mypy - run: poetry run mypy faststack_core/ cli/ - - test: - name: Test - runs-on: ubuntu-latest - timeout-minutes: 10 - if: github.event_name != 'pull_request' || !github.event.pull_request.draft - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - - - uses: actions/setup-python@v5 + - name: ๐Ÿ’พ Cache Poetry dependencies + uses: actions/cache@v4 with: - python-version: "3.12" + path: ~/.cache/pypoetry + key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }} + restore-keys: ${{ runner.os }}-poetry- - - name: Install Poetry - run: pipx install poetry - - - name: Install dependencies + - name: ๐Ÿ“ฆ Install dependencies run: poetry install --no-interaction - - name: Run tests - run: poetry run pytest -v --tb=short || test $? -eq 5 + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Test โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + - name: ๐Ÿงช Run tests with coverage + run: | + poetry run pytest \ + --cov \ + --cov-report=term \ + --cov-report=xml \ + --cov-fail-under=85 \ + -v --tb=short + + - name: ๐Ÿ“Š Upload coverage report + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-py${{ matrix.python }} + path: coverage.xml + # ========================================================================= + # ๐Ÿ“ฆ Package Build + # ========================================================================= package: name: Package Build runs-on: ubuntu-latest timeout-minutes: 5 if: github.event_name != 'pull_request' || !github.event.pull_request.draft + steps: - - uses: actions/checkout@v4 + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Setup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + - name: โฌ‡๏ธ Checkout code + uses: actions/checkout@v4 with: persist-credentials: false - - uses: actions/setup-python@v5 + - name: ๐Ÿ Setup Python + uses: actions/setup-python@v5 with: python-version: "3.12" - - name: Install Poetry + - name: ๐Ÿ“ฆ Install Poetry run: pipx install poetry - - name: Build package + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Build & Validate โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + - name: ๐Ÿ”จ Build distributions run: poetry build - - name: Validate package + - name: โœ… Validate package metadata (twine) run: | pip install twine twine check dist/* diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index a34ae96..63685b5 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -1,3 +1,25 @@ +# =============================================================== +# ๐Ÿ” Dependency Review โ€” Vulnerabilities & Licenses +# =============================================================== +# +# This workflow: +# - Diffs dependency changes introduced by PRs to `main` +# - **Fails** when a change introduces either: +# โ†ณ A vulnerability of severity >= MODERATE +# โ†ณ A dependency under a strong-copyleft license incompatible +# with this project's MIT license (see deny-list below) +# +# Only runs when dependency files change (pyproject.toml, poetry.lock). +# Skips draft PRs. +# +# References +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# - Marketplace: https://github.com/marketplace/actions/dependency-review +# - Source code: https://github.com/github/dependency-review-action (MIT) +# +# Author: Manav Gupta +# =============================================================== + name: Dependency Review on: @@ -6,10 +28,14 @@ on: paths: - "pyproject.toml" - "poetry.lock" + - ".github/workflows/dependency-review.yml" +# ----------------------------------------------------------------- +# Minimal permissions โ€” principle of least privilege +# ----------------------------------------------------------------- permissions: - contents: read - pull-requests: write + contents: read # for actions/checkout + pull-requests: write # post PR comment on failure jobs: dependency-review: @@ -17,13 +43,25 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 if: "!github.event.pull_request.draft" + steps: - - uses: actions/checkout@v4 + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Checkout โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + - name: โฌ‡๏ธ Checkout code + uses: actions/checkout@v4 with: persist-credentials: false - - name: Dependency Review + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Scan โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + - name: ๐Ÿ” Dependency & License gate uses: actions/dependency-review-action@v4 with: + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Vulnerability policy โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ fail-on-severity: moderate + + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ License policy โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + # Deny strong-copyleft licenses incompatible with MIT. + # LGPL/MPL are weak copyleft and allowed. deny-licenses: GPL-3.0, AGPL-3.0, SSPL-1.0 + + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ UX โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + comment-summary-in-pr: on-failure diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..77ef0ab --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,128 @@ +# =============================================================== +# ๐Ÿ” Lint & Static Analysis โ€” Code Quality Gate +# =============================================================== +# +# This workflow: +# - Runs ruff for linting and import sorting +# - Runs black for code formatting checks +# - Runs mypy for static type analysis +# +# Triggers on push to main and PRs. Skips draft PRs. +# Cancels stale runs on the same branch. +# +# References +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# - Ruff: https://docs.astral.sh/ruff/ +# - Black: https://black.readthedocs.io/ +# - Mypy: https://mypy.readthedocs.io/ +# +# Author: Manav Gupta +# =============================================================== + +name: Lint & Static Analysis + +on: + push: + branches: [main] + paths: + - "**.py" + - "pyproject.toml" + - ".github/workflows/lint.yml" + pull_request: + types: [opened, synchronize, ready_for_review] + branches: [main] + paths: + - "**.py" + - "pyproject.toml" + - ".github/workflows/lint.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# ----------------------------------------------------------------- +# Minimal permissions โ€” principle of least privilege +# ----------------------------------------------------------------- +permissions: + contents: read + +jobs: + + # ========================================================================= + # ๐Ÿ” Ruff + Black + # ========================================================================= + python-lint: + name: Ruff & Black + runs-on: ubuntu-latest + timeout-minutes: 5 + if: github.event_name != 'pull_request' || !github.event.pull_request.draft + + steps: + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Setup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + - name: โฌ‡๏ธ Checkout code + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: ๐Ÿ Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: ๐Ÿ“ฆ Install Poetry + run: pipx install poetry + + - name: ๐Ÿ’พ Cache Poetry dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pypoetry + key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }} + restore-keys: ${{ runner.os }}-poetry- + + - name: ๐Ÿ“ฆ Install dependencies + run: poetry install --no-interaction + + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Checks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + - name: ๐Ÿ” Ruff lint + run: poetry run ruff check . + + - name: ๐ŸŽจ Black format check + run: poetry run black --check . + + # ========================================================================= + # ๐Ÿ”Ž Type Check + # ========================================================================= + typecheck: + name: Mypy + runs-on: ubuntu-latest + timeout-minutes: 5 + if: github.event_name != 'pull_request' || !github.event.pull_request.draft + + steps: + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Setup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + - name: โฌ‡๏ธ Checkout code + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: ๐Ÿ Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: ๐Ÿ“ฆ Install Poetry + run: pipx install poetry + + - name: ๐Ÿ’พ Cache Poetry dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pypoetry + key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }} + restore-keys: ${{ runner.os }}-poetry- + + - name: ๐Ÿ“ฆ Install dependencies + run: poetry install --no-interaction + + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Check โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + - name: ๐Ÿ”Ž Mypy type check + run: poetry run mypy faststack_core/ cli/ diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..d941f7c --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,68 @@ +# =============================================================== +# โœ… Pre-commit Checks +# =============================================================== +# +# This workflow: +# - Runs all pre-commit hooks against the full codebase +# - Validates: trailing whitespace, EOF, YAML/TOML syntax, +# Python AST, debug statements, private key detection, +# ruff lint, black formatting +# +# Only runs on PRs (not push to main โ€” the lint workflow covers that). +# Skips draft PRs. Cancels stale runs. +# +# References +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# - Pre-commit: https://pre-commit.com/ +# - pre-commit/action: https://github.com/pre-commit/action +# +# Author: Manav Gupta +# =============================================================== + +name: Pre-commit Checks + +on: + pull_request: + types: [opened, synchronize, ready_for_review] + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# ----------------------------------------------------------------- +# Minimal permissions โ€” principle of least privilege +# ----------------------------------------------------------------- +permissions: + contents: read + +jobs: + pre-commit: + name: Run pre-commit hooks + runs-on: ubuntu-latest + timeout-minutes: 10 + if: "!github.event.pull_request.draft" + + steps: + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Setup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + - name: โฌ‡๏ธ Checkout code + uses: actions/checkout@v4 + with: + persist-credentials: false + fetch-depth: 0 + + - name: ๐Ÿ Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: ๐Ÿ’พ Cache pre-commit environments + uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: ${{ runner.os }}-pre-commit- + + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Check โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + - name: โœ… Run pre-commit hooks + uses: pre-commit/action@v3.0.1 diff --git a/.gitignore b/.gitignore index f996082..0f8111b 100644 --- a/.gitignore +++ b/.gitignore @@ -136,4 +136,4 @@ poetry.lock **/.DS_Store # VSC settings -.vscode/ \ No newline at end of file +.vscode/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b035e29 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,31 @@ +fail_fast: true + +repos: + # General file hygiene + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-json + - id: check-toml + - id: check-added-large-files + args: ['--maxkb=500'] + - id: check-ast + - id: debug-statements + - id: detect-private-key + - id: check-merge-conflict + + # Ruff โ€” lint + import sorting + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.6 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + + # Black โ€” code formatting + - repo: https://github.com/psf/black + rev: 26.3.1 + hooks: + - id: black diff --git a/Makefile b/Makefile index f3c070e..0366406 100644 --- a/Makefile +++ b/Makefile @@ -1,40 +1,166 @@ +# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +# FastStack โ€” Hybrid FastAPI Framework +# Runtime core + CLI generator for async FastAPI projects +# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +# +# Author: Manav Gupta +# Usage: run `make` or `make help` to view available targets +# +# help: FastStack (Hybrid FastAPI framework โ€” runtime core + CLI generator) +# + +SHELL := /bin/bash +.SHELLFLAGS := -eu -o pipefail -c + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Project variables +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +PROJECT_NAME := faststack +VENV_DIR := .venv +COVERAGE_MIN := 85 + +# Directories and files to clean +DIRS_TO_CLEAN := __pycache__ .pytest_cache .mypy_cache .ruff_cache \ + htmlcov dist build .eggs *.egg-info + +FILES_TO_CLEAN := .coverage coverage.xml + +# ============================================================================= +# DYNAMIC HELP +# ============================================================================= +.PHONY: help +help: + @grep "^# help\:" Makefile | grep -v grep | sed 's/\# help\: //' | sed 's/\# help\://' + .DEFAULT_GOAL := help -.PHONY: install test test-verbose test-single lint format typecheck check clean help +# ============================================================================= +# help: +# help: ๐ŸŒฑ VIRTUAL ENVIRONMENT & INSTALLATION +# ============================================================================= + +# help: venv - Create a fresh virtual environment +.PHONY: venv +venv: + python3 -m venv $(VENV_DIR) + $(VENV_DIR)/bin/pip install --upgrade pip -install: ## Install dependencies +# help: install - Install all dependencies +.PHONY: install +install: poetry install -test: ## Run tests - poetry run pytest --no-header -q || test $$? -eq 5 +# help: install-dev - Full dev setup (venv + deps + pre-commit hooks) +.PHONY: install-dev +install-dev: venv install + poetry run pre-commit install + @echo "" + @echo "โœ… Dev environment ready. Run 'make check' to verify." + +# help: update - Update dependencies to latest compatible versions +.PHONY: update +update: + poetry update + poetry run pre-commit autoupdate + +# ============================================================================= +# help: +# help: ๐Ÿงช TESTING +# ============================================================================= + +# help: test - Run tests with coverage (CI gate: fails if <$(COVERAGE_MIN)%) +.PHONY: test +test: + poetry run pytest --cov --cov-fail-under=$(COVERAGE_MIN) --no-header -q || test $$? -eq 5 -test-verbose: ## Run tests with verbose output - poetry run pytest -v +# help: test-verbose - Run tests with verbose output + coverage +.PHONY: test-verbose +test-verbose: + poetry run pytest --cov -test-single: ## Run a single test (usage: make test-single K=test_name) +# help: test-single - Run a single test (usage: make test-single K=test_name) +.PHONY: test-single +test-single: poetry run pytest -k "$(K)" -lint: ## Check linting (ruff + black) +# help: test-unit - Run only unit tests (core + templates) +.PHONY: test-unit +test-unit: + poetry run pytest -m unit --no-header -q + +# help: test-integration - Run only integration tests (CLI commands) +.PHONY: test-integration +test-integration: + poetry run pytest -m integration --no-header -q + +# help: test-e2e - Run only e2e tests (full workflow validation) +.PHONY: test-e2e +test-e2e: + poetry run pytest -m e2e --no-header -q + +# help: test-fast - Run all tests except slow ones +.PHONY: test-fast +test-fast: + poetry run pytest -m "not slow" --no-header -q + +# help: coverage - Generate HTML + XML coverage report +.PHONY: coverage +coverage: + poetry run pytest --cov --cov-report=html --cov-report=xml --no-header -q + @echo "Coverage report: htmlcov/index.html" + +# ============================================================================= +# help: +# help: ๐Ÿ” CODE QUALITY +# ============================================================================= + +# help: lint - Check linting (ruff + black) +.PHONY: lint +lint: poetry run ruff check . poetry run black --check . -format: ## Auto-format code (ruff fix + black) +# help: format - Auto-format code (ruff fix + black) +.PHONY: format +format: poetry run ruff check --fix . poetry run black . -typecheck: ## Run type checking +# help: typecheck - Run static type checking (mypy) +.PHONY: typecheck +typecheck: poetry run mypy faststack_core/ cli/ -check: lint typecheck test ## Run all checks (CI gate) +# help: check - Run all checks: lint + typecheck + test (CI gate) +.PHONY: check +check: lint typecheck test + +# help: pre-commit - Run pre-commit hooks on all files +.PHONY: pre-commit +pre-commit: + poetry run pre-commit run --all-files + +# help: pre-commit-install - Install pre-commit hooks into .git/hooks +.PHONY: pre-commit-install +pre-commit-install: + poetry run pre-commit install + +# ============================================================================= +# help: +# help: ๐Ÿงน CLEANUP +# ============================================================================= -clean: ## Remove build artifacts and caches - find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true - find . -type d -name .pytest_cache -exec rm -rf {} + 2>/dev/null || true - find . -type d -name .mypy_cache -exec rm -rf {} + 2>/dev/null || true - find . -type d -name .ruff_cache -exec rm -rf {} + 2>/dev/null || true - find . -type d -name htmlcov -exec rm -rf {} + 2>/dev/null || true - find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true - rm -rf dist/ build/ .coverage +# help: clean - Remove build artifacts and caches +.PHONY: clean +clean: + @echo "๐Ÿงน Cleaning workspace..." + @find . -type d \( -name __pycache__ -o -name .pytest_cache -o -name .mypy_cache -o -name .ruff_cache -o -name htmlcov -o -name "*.egg-info" \) -exec rm -rf {} + 2>/dev/null || true + @rm -rf dist/ build/ .coverage coverage.xml + @echo "โœ… Clean complete." -help: ## Show this help message - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' +# help: clean-all - Remove everything including virtual environment +.PHONY: clean-all +clean-all: clean + @echo "๐Ÿ—‘๏ธ Removing virtual environment..." + @rm -rf $(VENV_DIR) + @echo "โœ… Full clean complete." diff --git a/pyproject.toml b/pyproject.toml index de42511..558982a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,8 @@ aiosqlite = ">=0.20.0" httpx = ">=0.28.0" mypy = ">=1.13.0" types-pyyaml = "^6.0.12.20250915" +pytest-cov = "^7.1.0" +pre-commit = "^4.5.1" [build-system] requires = ["poetry-core"] @@ -46,6 +48,24 @@ build-backend = "poetry.core.masonry.api" asyncio_mode = "auto" testpaths = ["tests"] addopts = "-v --tb=short" +markers = [ + "unit: Fast isolated tests (core, templates)", + "integration: Tests that scaffold projects in tmp dirs (CLI commands)", + "e2e: End-to-end workflow tests (init โ†’ add-entity โ†’ generate โ†’ list)", + "slow: Tests that take >1s", +] + +[tool.coverage.run] +source = ["faststack_core", "cli"] +omit = ["tests/*"] + +[tool.coverage.report] +show_missing = true +exclude_lines = [ + "pragma: no cover", + "if TYPE_CHECKING:", + "if __name__ == .__main__.", +] [tool.ruff] target-version = "py312" diff --git a/tests/test_cli/conftest.py b/tests/test_cli/conftest.py new file mode 100644 index 0000000..f70703a --- /dev/null +++ b/tests/test_cli/conftest.py @@ -0,0 +1,9 @@ +"""Auto-mark all tests in test_cli/ as integration tests.""" + +import pytest + + +def pytest_collection_modifyitems(items): + for item in items: + if "/test_cli/" in str(item.fspath): + item.add_marker(pytest.mark.integration) diff --git a/tests/test_core/conftest.py b/tests/test_core/conftest.py new file mode 100644 index 0000000..738b821 --- /dev/null +++ b/tests/test_core/conftest.py @@ -0,0 +1,9 @@ +"""Auto-mark all tests in test_core/ as unit tests.""" + +import pytest + + +def pytest_collection_modifyitems(items): + for item in items: + if "/test_core/" in str(item.fspath): + item.add_marker(pytest.mark.unit) diff --git a/tests/test_e2e/__init__.py b/tests/test_e2e/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_e2e/conftest.py b/tests/test_e2e/conftest.py new file mode 100644 index 0000000..e5fe564 --- /dev/null +++ b/tests/test_e2e/conftest.py @@ -0,0 +1,9 @@ +"""Auto-mark all tests in test_e2e/ as e2e tests.""" + +import pytest + + +def pytest_collection_modifyitems(items): + for item in items: + if "/test_e2e/" in str(item.fspath): + item.add_marker(pytest.mark.e2e) diff --git a/tests/test_e2e/test_smoke.py b/tests/test_e2e/test_smoke.py new file mode 100644 index 0000000..638d4b1 --- /dev/null +++ b/tests/test_e2e/test_smoke.py @@ -0,0 +1,179 @@ +"""End-to-end smoke test: scaffold a project and validate all generated code. + +Covers issue #13: generated project validation. +When Phase 6 entity CLI commands land, this test should be extended to cover +the full init โ†’ add-entity โ†’ generate โ†’ list workflow. +""" + +from __future__ import annotations + +import ast +from pathlib import Path + +import pytest +import yaml +from click.testing import CliRunner + +from cli import cli_group + + +@pytest.fixture +def runner(): + return CliRunner() + + +@pytest.fixture +def entities_yaml(tmp_path: Path) -> Path: + """Write a multi-entity YAML fixture.""" + content = """\ +entities: + User: + base: FullAuditedEntity + fields: + email: + type: string + required: true + unique: true + name: + type: string + required: true + role: + type: enum + values: [admin, editor, viewer] + default: '"viewer"' + searchable: [email, name] + + Post: + base: AuditedEntity + fields: + title: + type: string + required: true + content: + type: text + user_id: + type: uuid + references: User + searchable: [title] + + Category: + base: AuditedEntity + fields: + name: + type: string + required: true + parent_id: + type: uuid + references: self +""" + yaml_file = tmp_path / "entities.yaml" + yaml_file.write_text(content) + return yaml_file + + +class TestProjectScaffoldSmoke: + """Scaffold a full project and validate the structure.""" + + def test_init_succeeds(self, runner: CliRunner, tmp_path: Path, monkeypatch) -> None: + monkeypatch.chdir(tmp_path) + result = runner.invoke(cli_group, ["init", "blog"], catch_exceptions=False) + assert result.exit_code == 0 + assert "Created project" in result.output + + def test_all_expected_directories_exist( + self, runner: CliRunner, tmp_path: Path, monkeypatch + ) -> None: + monkeypatch.chdir(tmp_path) + runner.invoke(cli_group, ["init", "blog"], catch_exceptions=False) + project = tmp_path / "blog" + + expected_dirs = [ + "app/models", + "app/schemas", + "app/repositories", + "app/services", + "app/api/routes", + "tests/unit/fakes", + "tests/integration", + "tests/factories", + "alembic/versions", + ] + for d in expected_dirs: + assert (project / d).is_dir(), f"Missing directory: {d}" + + def test_project_files_exist(self, runner: CliRunner, tmp_path: Path, monkeypatch) -> None: + monkeypatch.chdir(tmp_path) + runner.invoke(cli_group, ["init", "blog"], catch_exceptions=False) + project = tmp_path / "blog" + + expected_files = [ + "app/main.py", + "app/config.py", + "pyproject.toml", + "Dockerfile", + "docker-compose.yml", + "alembic.ini", + "alembic/env.py", + "tests/conftest.py", + ".project-config.yaml", + ".env", + ] + for f in expected_files: + assert (project / f).is_file(), f"Missing: {f}" + + def test_all_generated_python_is_valid( + self, runner: CliRunner, tmp_path: Path, monkeypatch + ) -> None: + monkeypatch.chdir(tmp_path) + runner.invoke(cli_group, ["init", "blog"], catch_exceptions=False) + project = tmp_path / "blog" + + py_files = list(project.rglob("*.py")) + assert len(py_files) >= 15, f"Expected >=15 .py files, got {len(py_files)}" + + for py_file in py_files: + source = py_file.read_text() + if not source.strip(): + continue # skip empty __init__.py + try: + ast.parse(source) + except SyntaxError as e: + rel = py_file.relative_to(project) + pytest.fail(f"Invalid Python in {rel}: {e}") + + def test_project_config_has_structure( + self, runner: CliRunner, tmp_path: Path, monkeypatch + ) -> None: + monkeypatch.chdir(tmp_path) + runner.invoke(cli_group, ["init", "blog"], catch_exceptions=False) + + config = yaml.safe_load((tmp_path / "blog" / ".project-config.yaml").read_text()) + assert config["project_name"] == "blog" + assert config["architecture"] == "simple" + assert "entities" in config + + +class TestInitWithEntitiesSmoke: + """Scaffold with --entities and validate router registration.""" + + def test_init_with_entities_has_routers_in_main( + self, + runner: CliRunner, + tmp_path: Path, + monkeypatch, + entities_yaml: Path, + ) -> None: + monkeypatch.chdir(tmp_path) + result = runner.invoke( + cli_group, + ["init", "blog", "--entities", str(entities_yaml)], + catch_exceptions=False, + ) + assert result.exit_code == 0 + + main_content = (tmp_path / "blog" / "app" / "main.py").read_text() + assert "user_router" in main_content + assert "post_router" in main_content + assert "category_router" in main_content + assert "include_router" in main_content + ast.parse(main_content) diff --git a/tests/test_templates/conftest.py b/tests/test_templates/conftest.py new file mode 100644 index 0000000..58c0e7b --- /dev/null +++ b/tests/test_templates/conftest.py @@ -0,0 +1,9 @@ +"""Auto-mark all tests in test_templates/ as unit tests.""" + +import pytest + + +def pytest_collection_modifyitems(items): + for item in items: + if "/test_templates/" in str(item.fspath): + item.add_marker(pytest.mark.unit)