diff --git a/.github/agents/coverage-agent.md b/.github/agents/coverage-agent.md new file mode 100644 index 00000000..d2cea718 --- /dev/null +++ b/.github/agents/coverage-agent.md @@ -0,0 +1,19 @@ +name: Coverage Agent +description: An agent that writes pytest tests to improve code coverage. +instructions: | + You are an expert Python QA engineer specializing in `pytest`. + Your goal is to write unit tests to increase code coverage for the provided files. + + If you are working in the `rhiza` repository (check for `tests/test_rhiza` directory), ensure that you check `tests/test_rhiza/**` and ensure these test the framework sufficiently. + + Process: + 1. Read the coverage report at `_tests/coverage.json`. + 2. Identify files with low coverage and analyze their source code. + 3. Write comprehensive `pytest` tests in `tests/` to cover missing lines. + 4. Ensure there is a GitHub Issue tracking this coverage gap. + 5. Create a new branch named `coverage/fix-${RUN_ID}` from `${PR_BRANCH}`. + 6. Commit the new tests to this branch. + 7. Push the branch and create a Pull Request into `${PR_BRANCH}`. + - Title: "chore: improve test coverage" + - Body: "Closes #." + diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..a8487319 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,47 @@ +# Rhiza Copilot Instructions + +You are working in a project that utilizes the `rhiza` framework. Rhiza is a collection of reusable configuration templates and tooling designed to standardize and streamline modern Python development. + +As a Rhiza-based project, this workspace adheres to specific conventions for structure, dependency management, and automation. + +## Development Environment + +The project uses `make` and `uv` for development tasks. + +- **Install Dependencies**: `make install` (installs `uv`, creates `.venv`, installs dependencies) +- **Run Tests**: `make test` (runs `pytest` with coverage) +- **Format Code**: `make fmt` (runs `ruff format` and `ruff check --fix`) +- **Check Dependencies**: `make deptry` (runs `deptry` to check for missing/unused dependencies) +- **Marimo Notebooks**: `make marimo` (starts the Marimo server) +- **Build Documentation**: `make book` (builds the documentation book) + +## Project Structure + +- `src/`: Source code +- `tests/`: Tests (pytest) +- `assets/`: Static assets +- `book/`: Documentation source +- `docker/`: Docker configuration +- `presentation/`: Presentation slides +- `.rhiza/`: Rhiza-specific scripts and configurations + +## Coding Standards + +- **Style**: Follow PEP 8. Use `make fmt` to enforce style. +- **Testing**: Write tests in `tests/` using `pytest`. Ensure high coverage. +- **Documentation**: Document code using docstrings. +- **Dependencies**: Manage dependencies in `pyproject.toml`. Use `uv add` to add dependencies. + +## Workflow + +1. **Setup**: Run `make install` to set up the environment. +2. **Develop**: Write code in `src/` and tests in `tests/`. +3. **Test**: Run `make test` to verify changes. +4. **Format**: Run `make fmt` before committing. +5. **Verify**: Run `make deptry` to check dependencies. + +## Key Files + +- `Makefile`: Main entry point for tasks. +- `pyproject.toml`: Project configuration and dependencies. +- `.devcontainer/bootstrap.sh`: Bootstrap script for dev containers. diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 00000000..4d8c5918 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,44 @@ +# Following the instructions at https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/customize-the-agent-environment#preinstalling-tools-or-dependencies-in-copilots-environment, create a workflow +# that defines the setup steps for Copilot Agents. + +name: "Copilot Setup Steps" + +# Automatically run the setup steps when they are changed to allow for easy validation, and +# allow manual testing through the repository's "Actions" tab +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. + copilot-setup-steps: + runs-on: ubuntu-latest + + # Set the permissions to the lowest permissions possible needed for your steps. + # Copilot will be given its own token for its operations. + permissions: + # If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete. + contents: read + + # You can define any steps you want, and they will run before the agent starts. + # If you do not check out your code, Copilot will do this for you. + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + make install + echo "$(pwd)/bin" >> $GITHUB_PATH + + # Initialize pre-commit hooks as per bootstrap.sh + if [ -f .pre-commit-config.yaml ]; then + ./bin/uvx pre-commit install + fi + + diff --git a/.github/workflows/rhiza_coverage-agent.yml b/.github/workflows/rhiza_coverage-agent.yml new file mode 100644 index 00000000..7e86f7fc --- /dev/null +++ b/.github/workflows/rhiza_coverage-agent.yml @@ -0,0 +1,139 @@ +name: RHIZA COVERAGE AGENT + +on: + pull_request: + branches: [ main, master ] + +permissions: + contents: read + issues: write + +jobs: + check-coverage: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v1 + with: + version: "latest" + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: ".python-version" + + - name: Run tests and generate coverage + run: | + make test + + - name: Check Coverage and Create Issue + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_ASSIGNEE: copilot + run: | + python -c " + import json + import subprocess + import sys + import os + + def ensure_label(name, color, description): + # Attempt to create label. If it fails (e.g. exists), we ignore the error. + subprocess.run( + ['gh', 'label', 'create', name, '--color', color, '--description', description], + capture_output=True + ) + + def create_issue(title, body, labels, assignee): + cmd = ['gh', 'issue', 'create', '--title', title, '--body', body] + for label in labels: + cmd.extend(['--label', label]) + + try: + # Create issue without assignee first to avoid failure if assignee is invalid + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + print('Issue created successfully.') + issue_url = result.stdout.strip() + + # Try to assign + if assignee: + print(f'Attempting to assign to {assignee}...') + assign_cmd = ['gh', 'issue', 'edit', issue_url, '--add-assignee', assignee] + assign_res = subprocess.run(assign_cmd, capture_output=True, text=True) + if assign_res.returncode != 0: + print(f'Warning: Could not assign to {assignee}: {assign_res.stderr}') + else: + print(f'Assigned to {assignee}.') + except subprocess.CalledProcessError as e: + print(f'Failed to create issue: {e.stderr}') + raise + + # Ensure labels exist + ensure_label('coverage', '0E8A16', 'Related to code coverage') + ensure_label('chore', 'EDEDED', 'Routine tasks and maintenance') + + def get_agent_instructions(): + try: + with open('.github/agents/coverage-agent.md', 'r') as f: + content = f.read() + # Simple parsing to extract instructions block + if 'instructions: |' in content: + return content.split('instructions: |')[1].strip() + return content + except Exception: + return "Please analyze the coverage report and add tests to reach 100% coverage." + + instructions = get_agent_instructions() + + coverage_path = '_tests/coverage.json' + has_report = os.path.exists(coverage_path) + repo = os.environ.get('GITHUB_REPOSITORY') + is_rhiza = repo == 'jebel-quant/rhiza' + assignee = os.environ.get('ISSUE_ASSIGNEE') + + print(f'Checking coverage report at {coverage_path}...') + print(f'Repository: {repo}') + + if not has_report: + print('Coverage report not found.') + if is_rhiza: + print('Rhiza repository detected. Creating issue for framework test review...') + title = 'Rhiza Framework Test Review Needed' + body = ( + 'The coverage report was not found, but this is the Rhiza repository.\n\n' + 'Please review \`tests/test_rhiza\` and ensure the framework is sufficiently tested.\n\n' + '@github-actions assign to copilot' + ) + create_issue(title, body, ['coverage', 'chore'], assignee) + else: + print('Not the Rhiza repository. Skipping.') + sys.exit(0) + + try: + with open(coverage_path) as f: + data = json.load(f) + + coverage = data['totals']['percent_covered'] + print(f'Current Coverage: {coverage:.2f}%') + + if coverage < 100: + print('Coverage is below 100%. Creating GitHub Issue...') + + title = f'Coverage Gap Detected: {coverage:.2f}%' + body = ( + f'Coverage analysis detected gaps. Current coverage is {coverage:.2f}%.\n\n' + f'{instructions}\n\n' + '@github-actions assign to copilot' + ) + + create_issue(title, body, ['coverage', 'chore'], assignee) + else: + print('Coverage is 100%. No issue needed.') + + except Exception as e: + print(f'Error: {e}') + sys.exit(1) + " diff --git a/README.md b/README.md index 55fd19cf..a5fcd599 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ Meta Development and Testing test run all tests benchmark run performance benchmarks + show-coverage open the coverage report in the browser Documentation docs create documentation with pdoc diff --git a/tests/Makefile.tests b/tests/Makefile.tests index 141fe90c..45fcdfda 100644 --- a/tests/Makefile.tests +++ b/tests/Makefile.tests @@ -2,7 +2,7 @@ # This file is included by the main Makefile # Declare phony targets (they don't produce files) -.PHONY: test benchmark +.PHONY: test benchmark show-coverage # Test-specific variables TESTS_FOLDER := tests @@ -29,3 +29,17 @@ benchmark: install ## run performance benchmarks printf "${YELLOW}[WARN] Benchmarks folder not found, skipping benchmarks${RESET}\n"; \ fi +show-coverage: ## open the coverage report in the browser + @if [ -f _tests/html-coverage/index.html ]; then \ + if command -v open >/dev/null 2>&1; then \ + open _tests/html-coverage/index.html; \ + elif command -v xdg-open >/dev/null 2>&1; then \ + xdg-open _tests/html-coverage/index.html; \ + else \ + printf "${YELLOW}[WARN] Could not open browser. Please open _tests/html-coverage/index.html manually.${RESET}\n"; \ + fi \ + else \ + printf "${YELLOW}[WARN] Coverage report not found. Run 'make test' first.${RESET}\n"; \ + fi + +