diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index b2be07a..672d700 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -40,9 +40,57 @@ jobs: - name: Run tests with pytest run: | # Run tests with coverage - python -m pytest tests/ -v --cov=pyWebLayout --cov-report=term-missing + python -m pytest tests/ -v --cov=pyWebLayout --cov-report=term-missing --cov-report=json --cov-report=html + + - name: Generate test coverage badge + run: | + # Install coverage-badge for generating badges + pip install coverage-badge + # Generate coverage badge from coverage data + coverage-badge -o coverage.svg + + - name: Check documentation coverage + run: | + # Install interrogate for documentation coverage + pip install interrogate + # Generate documentation coverage report and badge + interrogate -v --ignore-init-method --ignore-init-module --ignore-magic --ignore-private --ignore-property-decorators --ignore-semiprivate --fail-under=80 --generate-badge coverage-docs.svg pyWebLayout/ + + - name: Generate coverage reports + run: | + # Generate coverage summary for README + python -c " +import json +import os + +# Read coverage data +if os.path.exists('coverage.json'): + with open('coverage.json', 'r') as f: + coverage_data = json.load(f) + + total_coverage = round(coverage_data['totals']['percent_covered'], 1) + + # Create coverage summary file + with open('coverage-summary.txt', 'w') as f: + f.write(f'{total_coverage}%') + + print(f'Test Coverage: {total_coverage}%') +else: + print('No coverage data found') +" + + - name: Upload coverage artifacts + uses: actions/upload-artifact@v3 + with: + name: coverage-reports + path: | + coverage.svg + coverage-docs.svg + htmlcov/ + coverage.json + coverage-summary.txt - name: Test package installation run: | # Test that the package can be imported - python -c "import pyWebLayout; print('Package imported successfully')" \ No newline at end of file + python -c "import pyWebLayout; print('Package imported successfully')" diff --git a/scripts/run_coverage.py b/scripts/run_coverage.py new file mode 100644 index 0000000..52cfb94 --- /dev/null +++ b/scripts/run_coverage.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +""" +Local coverage runner script. +Runs test and documentation coverage locally and generates badges. +""" + +import subprocess +import sys +import os + + +def run_command(cmd, description): + """Run a command and handle errors.""" + print(f"\n{'='*50}") + print(f"Running: {description}") + print(f"Command: {cmd}") + print(f"{'='*50}") + + try: + result = subprocess.run(cmd, shell=True, check=True, capture_output=True, text=True) + print(result.stdout) + if result.stderr: + print("STDERR:", result.stderr) + return True + except subprocess.CalledProcessError as e: + print(f"Error running {description}:") + print(f"Return code: {e.returncode}") + print(f"STDOUT: {e.stdout}") + print(f"STDERR: {e.stderr}") + return False + + +def main(): + """Run full coverage analysis locally.""" + print("Local Coverage Analysis for pyWebLayout") + print("=" * 60) + + # Change to project root if running from scripts directory + if os.path.basename(os.getcwd()) == "scripts": + os.chdir("..") + + # Install required packages + print("\n1. Installing required packages...") + packages = [ + "pytest pytest-cov", + "coverage-badge", + "interrogate" + ] + + for package in packages: + if not run_command(f"pip install {package}", f"Installing {package}"): + print(f"Failed to install {package}, continuing...") + + # Run tests with coverage + print("\n2. Running tests with coverage...") + test_cmd = "python -m pytest tests/ -v --cov=pyWebLayout --cov-report=term-missing --cov-report=json --cov-report=html" + run_command(test_cmd, "Running tests with coverage") + + # Generate test coverage badge + print("\n3. Generating test coverage badge...") + run_command("coverage-badge -o coverage.svg", "Generating test coverage badge") + + # Check documentation coverage + print("\n4. Checking documentation coverage...") + docs_cmd = "interrogate -v --ignore-init-method --ignore-init-module --ignore-magic --ignore-private --ignore-property-decorators --ignore-semiprivate --fail-under=80 --generate-badge coverage-docs.svg pyWebLayout/" + run_command(docs_cmd, "Checking documentation coverage") + + # Generate coverage summary + print("\n5. Generating coverage summary...") + summary_script = """ +import json +import os + +if os.path.exists('coverage.json'): + with open('coverage.json', 'r') as f: + coverage_data = json.load(f) + + total_coverage = round(coverage_data['totals']['percent_covered'], 1) + + with open('coverage-summary.txt', 'w') as f: + f.write(f'{total_coverage}%') + + print(f'Test Coverage: {total_coverage}%') + print(f'Lines Covered: {coverage_data["totals"]["covered_lines"]}/{coverage_data["totals"]["num_statements"]}') +else: + print('No coverage data found') +""" + run_command(f'python -c "{summary_script}"', "Generating coverage summary") + + # List generated files + print("\n6. Generated files:") + files = ["coverage.svg", "coverage-docs.svg", "coverage-summary.txt", "htmlcov/", "coverage.json"] + for file in files: + if os.path.exists(file): + print(f" ✓ {file}") + else: + print(f" ✗ {file} (not found)") + + print("\n" + "="*60) + print("Coverage analysis complete!") + print("To update your README with badges, run:") + print(" python scripts/update_coverage_badges.py") + print("\nTo view detailed HTML coverage report:") + print(" open htmlcov/index.html") + + +if __name__ == "__main__": + main() diff --git a/scripts/update_coverage_badges.py b/scripts/update_coverage_badges.py new file mode 100644 index 0000000..1763061 --- /dev/null +++ b/scripts/update_coverage_badges.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +""" +Script to update README.md with coverage badges from CI artifacts. +This script should be run after CI completes to update the badges in your README. +""" + +import os +import re +import sys + + +def update_readme_badges(): + """Update README.md with coverage badges.""" + readme_path = "README.md" + + if not os.path.exists(readme_path): + print("README.md not found!") + return False + + # Read current README + with open(readme_path, 'r') as f: + content = f.read() + + # Coverage badges to add/update + test_coverage_badge = "![Test Coverage](./coverage.svg)" + docs_coverage_badge = "![Documentation Coverage](./coverage-docs.svg)" + + # Check if badges already exist and update them, otherwise add them at the top + if "![Test Coverage]" in content: + content = re.sub(r'!\[Test Coverage\]\([^)]+\)', test_coverage_badge, content) + else: + # Add after the first line (title) + lines = content.split('\n') + if len(lines) > 0: + lines.insert(1, f"\n{test_coverage_badge}") + content = '\n'.join(lines) + + if "![Documentation Coverage]" in content: + content = re.sub(r'!\[Documentation Coverage\]\([^)]+\)', docs_coverage_badge, content) + else: + # Add after test coverage badge + lines = content.split('\n') + for i, line in enumerate(lines): + if "![Test Coverage]" in line: + lines.insert(i + 1, docs_coverage_badge) + break + content = '\n'.join(lines) + + # Write updated README + with open(readme_path, 'w') as f: + f.write(content) + + print("README.md updated with coverage badges!") + return True + + +def show_coverage_summary(): + """Display coverage summary if available.""" + if os.path.exists("coverage-summary.txt"): + with open("coverage-summary.txt", 'r') as f: + test_coverage = f.read().strip() + print(f"Current Test Coverage: {test_coverage}") + + # Try to get documentation coverage from interrogate output + if os.path.exists("coverage.json"): + import json + try: + with open("coverage.json", 'r') as f: + coverage_data = json.load(f) + print(f"Detailed Coverage: {coverage_data['totals']['percent_covered']:.1f}%") + print(f"Lines Covered: {coverage_data['totals']['covered_lines']}/{coverage_data['totals']['num_statements']}") + except (KeyError, json.JSONDecodeError): + print("Could not parse coverage data") + + +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == "--summary": + show_coverage_summary() + else: + update_readme_badges() + show_coverage_summary()