Test Coverage
Introduction
Test coverage measures how much of your code is executed during testing. It's a crucial metric for understanding the effectiveness of your test suite and identifying untested code paths.
Python provides several powerful tools for measuring and analyzing test coverage:
- coverage.py - The standard Python coverage measurement tool
- pytest-cov - pytest plugin for coverage reporting
- Coverage reporting - HTML, XML, and terminal reports
- Branch coverage - Beyond line coverage to decision points
Coverage helps you:
- Identify untested code
- Improve test quality
- Meet coverage requirements
- Find dead code
- Ensure critical paths are tested
Understanding coverage is essential for maintaining high-quality, well-tested Python applications.
Concepts
Types of Coverage
- Line Coverage - Percentage of code lines executed
- Branch Coverage - Percentage of code branches taken
- Function Coverage - Percentage of functions called
- Statement Coverage - Percentage of statements executed
Coverage Tools
- coverage.py - Core Python coverage library
- pytest-cov - Integration with pytest
- Coverage reports - Multiple output formats
- Configuration - .coveragerc files for setup
Coverage Metrics
- Percentage - Overall coverage score
- Missing lines - Uncovered code locations
- Branch analysis - Decision point coverage
- Exclusions - Code intentionally skipped
Best Practices
- Aim for high coverage but focus on quality
- Cover edge cases and error conditions
- Use branch coverage for better insights
- Exclude irrelevant code appropriately
Examples
Installing Coverage Tools
# Install coverage.py
pip install coverage
# Install pytest-cov (includes coverage.py)
pip install pytest-cov
# Install together
pip install pytest pytest-cov coverage
Basic Coverage with coverage.py
# calculator.py
"""A simple calculator module for coverage demonstration"""
class Calculator:
"""Basic calculator with various operations"""
def __init__(self):
self.history = []
self.memory = 0
def add(self, a, b):
"""Add two numbers"""
result = a + b
self.history.append(f"{a} + {b} = {result}")
return result
def subtract(self, a, b):
"""Subtract b from a"""
result = a - b
self.history.append(f"{a} - {b} = {result}")
return result
def multiply(self, a, b):
"""Multiply two numbers"""
result = a * b
self.history.append(f"{a} * {b} = {result}")
return result
def divide(self, a, b):
"""Divide a by b"""
if b == 0:
raise ValueError("Cannot divide by zero")
result = a / b
self.history.append(f"{a} / {b} = {result}")
return result
def power(self, base, exponent):
"""Raise base to the power of exponent"""
result = base ** exponent
self.history.append(f"{base} ^ {exponent} = {result}")
return result
def square_root(self, number):
"""Calculate square root"""
if number < 0:
raise ValueError("Cannot calculate square root of negative number")
result = number ** 0.5
self.history.append(f"√{number} = {result}")
return result
def memory_store(self, value):
"""Store value in memory"""
self.memory = value
return f"Stored {value} in memory"
def memory_recall(self):
"""Recall value from memory"""
return self.memory
def memory_clear(self):
"""Clear memory"""
self.memory = 0
return "Memory cleared"
def get_history(self, limit=None):
"""Get calculation history"""
if limit:
return self.history[-limit:]
return self.history.copy()
def clear_history(self):
"""Clear calculation history"""
self.history.clear()
return "History cleared"
def factorial(self, n):
"""Calculate factorial (recursive implementation)"""
if not isinstance(n, int) or n < 0:
raise ValueError("Factorial only works with non-negative integers")
if n == 0 or n == 1:
return 1
else:
result = n * self.factorial(n - 1)
self.history.append(f"{n}! = {result}")
return result
def calculate_bmi(weight, height):
"""Calculate Body Mass Index"""
if weight <= 0 or height <= 0:
raise ValueError("Weight and height must be positive")
bmi = weight / (height ** 2)
if bmi < 18.5:
category = "Underweight"
elif bmi < 25:
category = "Normal weight"
elif bmi < 30:
category = "Overweight"
else:
category = "Obese"
return {
'bmi': round(bmi, 2),
'category': category,
'weight': weight,
'height': height
}
def fibonacci_sequence(n):
"""Generate Fibonacci sequence up to n terms"""
if n <= 0:
return []
elif n == 1:
return [0]
elif n == 2:
return [0, 1]
sequence = [0, 1]
for i in range(2, n):
sequence.append(sequence[i-1] + sequence[i-2])
return sequence
def is_prime(number):
"""Check if a number is prime"""
if number < 2:
return False
if number == 2:
return True
if number % 2 == 0:
return False
for i in range(3, int(number ** 0.5) + 1, 2):
if number % i == 0:
return False
return True
Test File with Partial Coverage
# test_calculator.py
"""Test file for calculator module - demonstrating coverage"""
import pytest
from calculator import Calculator, calculate_bmi, fibonacci_sequence, is_prime
class TestCalculator:
"""Test cases for Calculator class"""
def test_addition(self):
"""Test addition operation"""
calc = Calculator()
assert calc.add(2, 3) == 5
assert calc.add(-1, 1) == 0
assert calc.add(0, 0) == 0
def test_subtraction(self):
"""Test subtraction operation"""
calc = Calculator()
assert calc.subtract(5, 3) == 2
assert calc.subtract(1, 1) == 0
assert calc.subtract(-1, -1) == 0
def test_multiplication(self):
"""Test multiplication operation"""
calc = Calculator()
assert calc.multiply(3, 4) == 12
assert calc.multiply(-2, 3) == -6
assert calc.multiply(0, 5) == 0
def test_division_success(self):
"""Test successful division"""
calc = Calculator()
assert calc.divide(6, 2) == 3
assert calc.divide(7, 2) == 3.5
def test_division_by_zero(self):
"""Test division by zero raises exception"""
calc = Calculator()
with pytest.raises(ValueError, match="Cannot divide by zero"):
calc.divide(5, 0)
def test_power(self):
"""Test power operation"""
calc = Calculator()
assert calc.power(2, 3) == 8
assert calc.power(5, 2) == 25
assert calc.power(10, 0) == 1
def test_square_root_success(self):
"""Test successful square root"""
calc = Calculator()
assert calc.square_root(4) == 2
assert calc.square_root(9) == 3
assert calc.square_root(0) == 0
def test_square_root_negative(self):
"""Test square root of negative number"""
calc = Calculator()
with pytest.raises(ValueError, match="Cannot calculate square root of negative number"):
calc.square_root(-4)
def test_memory_operations(self):
"""Test memory store, recall, and clear"""
calc = Calculator()
# Test store and recall
calc.memory_store(42)
assert calc.memory_recall() == 42
# Test clear
calc.memory_clear()
assert calc.memory_recall() == 0
def test_history_operations(self):
"""Test history functionality"""
calc = Calculator()
# Perform some operations
calc.add(1, 2)
calc.multiply(3, 4)
history = calc.get_history()
assert len(history) == 2
assert "1 + 2 = 3" in history
assert "3 * 4 = 12" in history
# Test limited history
limited = calc.get_history(1)
assert len(limited) == 1
# Clear history
calc.clear_history()
assert len(calc.get_history()) == 0
class TestUtilityFunctions:
"""Test cases for utility functions"""
def test_calculate_bmi_normal(self):
"""Test BMI calculation for normal weight"""
result = calculate_bmi(70, 1.75) # kg, meters
assert result['bmi'] == 22.86
assert result['category'] == 'Normal weight'
def test_calculate_bmi_invalid_input(self):
"""Test BMI calculation with invalid input"""
with pytest.raises(ValueError, match="Weight and height must be positive"):
calculate_bmi(-70, 1.75)
with pytest.raises(ValueError, match="Weight and height must be positive"):
calculate_bmi(70, 0)
def test_fibonacci_sequence(self):
"""Test Fibonacci sequence generation"""
assert fibonacci_sequence(0) == []
assert fibonacci_sequence(1) == [0]
assert fibonacci_sequence(2) == [0, 1]
assert fibonacci_sequence(5) == [0, 1, 1, 2, 3]
def test_is_prime(self):
"""Test prime number checking"""
assert is_prime(2) == True
assert is_prime(3) == True
assert is_prime(4) == False
assert is_prime(17) == True
assert is_prime(1) == False
assert is_prime(0) == False
# Note: Some functions are not tested yet:
# - Calculator.factorial()
# - BMI categories other than normal weight
# - Edge cases in prime checking
Running Coverage with coverage.py
# Run tests with coverage
coverage run -m pytest test_calculator.py
# Generate coverage report
coverage report
# Generate detailed HTML report
coverage html
# Generate XML report (for CI/CD)
coverage xml
# Show missing lines
coverage report --show-missing
Using pytest-cov Plugin
# Run tests with coverage using pytest-cov
pytest --cov=calculator test_calculator.py
# Generate HTML report
pytest --cov=calculator --cov-report=html test_calculator.py
# Show missing lines
pytest --cov=calculator --cov-report=term-missing test_calculator.py
# Generate multiple report formats
pytest --cov=calculator --cov-report=html --cov-report=xml --cov-report=term test_calculator.py
# Include branch coverage
pytest --cov=calculator --cov-branch test_calculator.py
Coverage Configuration File
# .coveragerc
[run]
# Specify source code directory
source = .
# Omit certain files or patterns
omit =
*/tests/*
*/test_*
setup.py
*/venv/*
*/.venv/*
*/virtualenv/*
*/__pycache__/*
*/migrations/*
# Include branch coverage
branch = True
[report]
# Exclude lines from coverage
exclude_lines =
# Have to re-enable the standard pragma
pragma: no cover
# Don't complain about missing debug-only code:
def __repr__
if self\.debug
# Don't complain if tests don't hit defensive assertion code:
raise AssertionError
raise NotImplementedError
# Don't complain if non-runnable code isn't run:
if 0:
if __name__ == .__main__.:
# Don't complain about abstract methods
@(abc\.)?abstractmethod
# Fail if coverage is below this threshold
fail_under = 80
# Show missing line numbers
show_missing = True
# Skip covered files in report
skip_covered = False
[html]
# Directory for HTML coverage reports
directory = htmlcov
[xml]
# Filename for XML coverage report
output = coverage.xml
Complete Test Suite for Full Coverage
# test_calculator_complete.py
"""Complete test suite demonstrating full coverage"""
import pytest
from calculator import Calculator, calculate_bmi, fibonacci_sequence, is_prime
class TestCalculatorComplete:
"""Complete test coverage for Calculator class"""
def test_all_basic_operations(self):
"""Test all basic calculator operations"""
calc = Calculator()
# Test all operations
assert calc.add(2, 3) == 5
assert calc.subtract(5, 2) == 3
assert calc.multiply(4, 3) == 12
assert calc.divide(8, 2) == 4
assert calc.power(2, 3) == 8
assert calc.square_root(16) == 4
def test_division_by_zero(self):
"""Test division by zero error"""
calc = Calculator()
with pytest.raises(ValueError, match="Cannot divide by zero"):
calc.divide(10, 0)
def test_square_root_negative(self):
"""Test square root of negative number"""
calc = Calculator()
with pytest.raises(ValueError, match="Cannot calculate square root of negative number"):
calc.square_root(-9)
def test_factorial_valid(self):
"""Test factorial with valid inputs"""
calc = Calculator()
assert calc.factorial(0) == 1
assert calc.factorial(1) == 1
assert calc.factorial(5) == 120
def test_factorial_invalid(self):
"""Test factorial with invalid inputs"""
calc = Calculator()
with pytest.raises(ValueError, match="Factorial only works with non-negative integers"):
calc.factorial(-1)
with pytest.raises(ValueError, match="Factorial only works with non-negative integers"):
calc.factorial(3.5)
def test_memory_operations_complete(self):
"""Complete test of memory operations"""
calc = Calculator()
# Store value
result = calc.memory_store(42)
assert result == "Stored 42 in memory"
assert calc.memory_recall() == 42
# Clear memory
result = calc.memory_clear()
assert result == "Memory cleared"
assert calc.memory_recall() == 0
def test_history_operations_complete(self):
"""Complete test of history operations"""
calc = Calculator()
# Initially empty
assert calc.get_history() == []
# Add some operations
calc.add(1, 2)
calc.subtract(5, 3)
calc.multiply(2, 4)
history = calc.get_history()
assert len(history) == 3
# Test limited history
limited = calc.get_history(2)
assert len(limited) == 2
assert limited == history[-2:]
# Test with None limit
full = calc.get_history(None)
assert full == history
# Clear history
result = calc.clear_history()
assert result == "History cleared"
assert calc.get_history() == []
class TestBMIComplete:
"""Complete test coverage for BMI calculation"""
def test_all_bmi_categories(self):
"""Test all BMI categories"""
# Underweight
result = calculate_bmi(45, 1.75) # BMI ≈ 14.7
assert result['category'] == 'Underweight'
# Normal weight
result = calculate_bmi(70, 1.75) # BMI ≈ 22.9
assert result['category'] == 'Normal weight'
# Overweight
result = calculate_bmi(80, 1.75) # BMI ≈ 26.1
assert result['category'] == 'Overweight'
# Obese
result = calculate_bmi(100, 1.75) # BMI ≈ 32.7
assert result['category'] == 'Obese'
def test_bmi_edge_cases(self):
"""Test BMI calculation edge cases"""
# Exactly at category boundaries
result = calculate_bmi(56.7, 1.75) # BMI = 18.5
assert result['category'] == 'Normal weight'
result = calculate_bmi(76.6, 1.75) # BMI = 25.0
assert result['category'] == 'Overweight'
result = calculate_bmi(91.9, 1.75) # BMI = 30.0
assert result['category'] == 'Obese'
def test_bmi_invalid_inputs(self):
"""Test BMI calculation with invalid inputs"""
with pytest.raises(ValueError, match="Weight and height must be positive"):
calculate_bmi(0, 1.75)
with pytest.raises(ValueError, match="Weight and height must be positive"):
calculate_bmi(70, 0)
with pytest.raises(ValueError, match="Weight and height must be positive"):
calculate_bmi(-70, 1.75)
with pytest.raises(ValueError, match="Weight and height must be positive"):
calculate_bmi(70, -1.75)
class TestFibonacciComplete:
"""Complete test coverage for Fibonacci sequence"""
def test_fibonacci_all_cases(self):
"""Test Fibonacci sequence for all cases"""
# Edge cases
assert fibonacci_sequence(0) == []
assert fibonacci_sequence(-1) == []
assert fibonacci_sequence(-5) == []
# Base cases
assert fibonacci_sequence(1) == [0]
assert fibonacci_sequence(2) == [0, 1]
# Normal cases
assert fibonacci_sequence(3) == [0, 1, 1]
assert fibonacci_sequence(8) == [0, 1, 1, 2, 3, 5, 8, 13]
class TestPrimeComplete:
"""Complete test coverage for prime number checking"""
def test_prime_all_cases(self):
"""Test prime checking for all cases"""
# Less than 2
assert is_prime(-1) == False
assert is_prime(0) == False
assert is_prime(1) == False
# Prime number 2
assert is_prime(2) == True
# Even numbers > 2
assert is_prime(4) == False
assert is_prime(6) == False
assert is_prime(100) == False
# Odd primes
assert is_prime(3) == True
assert is_prime(5) == True
assert is_prime(7) == True
assert is_prime(11) == True
assert is_prime(17) == True
# Odd non-primes
assert is_prime(9) == False # 3 * 3
assert is_prime(15) == False # 3 * 5
assert is_prime(21) == False # 3 * 7
assert is_prime(25) == False # 5 * 5
Advanced Coverage Analysis
# coverage_analysis.py
"""Script to analyze coverage and generate reports"""
import subprocess
import json
import os
def run_coverage_analysis():
"""Run comprehensive coverage analysis"""
print("Running coverage analysis...")
# Run tests with coverage
result = subprocess.run([
'pytest', '--cov=calculator', '--cov-branch',
'--cov-report=json', '--cov-report=html',
'--cov-report=term-missing',
'test_calculator_complete.py'
], capture_output=True, text=True)
print("Coverage test results:")
print(result.stdout)
if result.stderr:
print("Errors:")
print(result.stderr)
# Parse coverage JSON report
if os.path.exists('coverage.json'):
with open('coverage.json', 'r') as f:
coverage_data = json.load(f)
print("\nCoverage Summary:")
print(f"Total Coverage: {coverage_data['totals']['percent_covered']:.2f}%")
print(f"Lines Covered: {coverage_data['totals']['covered_lines']}")
print(f"Lines Missing: {coverage_data['totals']['missing_lines']}")
print(f"Branches Covered: {coverage_data['totals']['covered_branches']}")
print(f"Branches Missing: {coverage_data['totals']['missing_branches']}")
# File-by-file breakdown
print("\nFile Coverage:")
for filename, file_data in coverage_data['files'].items():
if 'test_' not in filename:
coverage_percent = file_data['summary']['percent_covered']
print(f" {filename}: {coverage_percent:.2f}%")
if file_data['missing_lines']:
print(f" Missing lines: {file_data['missing_lines']}")
return result.returncode == 0
def check_coverage_threshold(threshold=90):
"""Check if coverage meets threshold"""
if os.path.exists('coverage.json'):
with open('coverage.json', 'r') as f:
coverage_data = json.load(f)
total_coverage = coverage_data['totals']['percent_covered']
if total_coverage >= threshold:
print(f"✅ Coverage {total_coverage:.2f}% meets threshold {threshold}%")
return True
else:
print(f"❌ Coverage {total_coverage:.2f}% below threshold {threshold}%")
return False
print("❌ No coverage data found")
return False
if __name__ == "__main__":
success = run_coverage_analysis()
if success:
check_coverage_threshold(90)
else:
print("Coverage analysis failed")
Challenges
1. Improve Test Coverage
Add tests to achieve 100% coverage for the calculator module.
💡 Show Answer
Looking at the coverage report, we need to test:
# Additional tests needed for 100% coverage
def test_missing_coverage():
"""Test previously uncovered code paths"""
calc = Calculator()
# Test factorial recursive path (n > 1)
assert calc.factorial(3) == 6
assert calc.factorial(4) == 24
# Test history with operations that add to history
calc.factorial(3)
history = calc.get_history()
assert any("3! = 6" in entry for entry in history)
# Test power operation edge cases
assert calc.power(0, 0) == 1 # Mathematical convention
assert calc.power(5, 1) == 5
def test_all_bmi_boundary_conditions():
"""Test BMI boundary conditions"""
# Test exact boundaries
calc_underweight = calculate_bmi(56.69, 1.75) # BMI just under 18.5
assert calc_underweight['category'] == 'Underweight'
calc_normal = calculate_bmi(56.7, 1.75) # BMI exactly 18.5
assert calc_normal['category'] == 'Normal weight'
calc_overweight = calculate_bmi(76.5, 1.75) # BMI just under 25
assert calc_overweight['category'] == 'Normal weight'
calc_overweight2 = calculate_bmi(76.6, 1.75) # BMI exactly 25
assert calc_overweight2['category'] == 'Overweight'
def test_prime_comprehensive():
"""Test prime function comprehensively"""
# Test the square root loop
assert is_prime(49) == False # 7 * 7
assert is_prime(121) == False # 11 * 11
# Test larger primes to hit the loop
assert is_prime(97) == True
assert is_prime(101) == True
Run with:
pytest --cov=calculator --cov-report=html --cov-branch test_complete_coverage.py
2. Coverage Configuration
Create a complete coverage configuration for a Python project.
💡 Show Answer
# .coveragerc - Complete configuration
[run]
# Source code to measure
source = src/
# Files to omit from coverage
omit =
*/tests/*
*/test_*
test_*.py
setup.py
conftest.py
*/venv/*
*/.venv/*
*/virtualenv/*
*/__pycache__/*
*/migrations/*
*/static/*
*/templates/*
manage.py
# Include branch coverage
branch = True
# Parallel processing for coverage
parallel = True
# Data file location
data_file = .coverage
[report]
# Regexes for lines to exclude from consideration
exclude_lines =
# Have to re-enable the standard pragma
pragma: no cover
# Don't complain about missing debug-only code:
def __repr__
if self\.debug
if settings\.DEBUG
# Don't complain if tests don't hit defensive assertion code:
raise AssertionError
raise NotImplementedError
# Don't complain if non-runnable code isn't run:
if 0:
if __name__ == .__main__.:
if TYPE_CHECKING:
# Don't complain about abstract methods
@(abc\.)?abstractmethod
# Don't complain about platform-specific code
except ImportError:
except AttributeError:
# Minimum coverage percentage
fail_under = 85
# Show line numbers of missing coverage
show_missing = True
# Skip files that are 100% covered
skip_covered = False
# Skip files that have no executable code
skip_empty = True
# Sort the report
sort = Cover
# Precision for percentages
precision = 2
[html]
# HTML output directory
directory = htmlcov
# Title for HTML report
title = Project Coverage Report
# Skip files that are 100% covered in HTML report
skip_covered = True
# Skip files that have no executable code in HTML report
skip_empty = True
[xml]
# XML output file
output = coverage.xml
[json]
# JSON output file
output = coverage.json
# Pretty print JSON
pretty_print = true
# pytest.ini or pyproject.toml configuration
[tool.pytest.ini_options]
addopts = [
"--cov=src",
"--cov-branch",
"--cov-report=html",
"--cov-report=term-missing",
"--cov-report=xml",
"--cov-fail-under=85",
"--strict-markers",
"--strict-config",
]
# Test discovery
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
# Markers
markers = [
"slow: marks tests as slow",
"integration: marks tests as integration tests",
"unit: marks tests as unit tests",
]
#!/bin/bash
# coverage_check.sh - Coverage verification script
set -e
echo "Running test coverage analysis..."
# Clean previous coverage data
coverage erase
# Run tests with coverage
pytest --cov=src --cov-branch --cov-report=html --cov-report=xml --cov-report=term
# Check coverage threshold
coverage report --fail-under=85
# Generate coverage badge (if coverage-badge is installed)
if command -v coverage-badge &> /dev/null; then
coverage-badge -o coverage.svg
echo "Coverage badge generated: coverage.svg"
fi
echo "Coverage analysis complete!"
echo "HTML report available at: htmlcov/index.html"
3. CI/CD Coverage Integration
Set up coverage reporting for continuous integration.
💡 Show Answer
# .github/workflows/test-coverage.yml
name: Test Coverage
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test-coverage:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9, 3.10, 3.11]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov coverage[toml]
pip install -r requirements.txt
- name: Run tests with coverage
run: |
pytest --cov=src --cov-branch --cov-report=xml --cov-report=html --cov-report=term-missing
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: true
- name: Upload coverage to Coveralls
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: coverage.xml
- name: Archive coverage reports
uses: actions/upload-artifact@v3
with:
name: coverage-report-${{ matrix.python-version }}
path: htmlcov/
- name: Coverage comment
uses: py-cov-action/python-coverage-comment-action@v3
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MINIMUM_GREEN: 90
MINIMUM_ORANGE: 70
# coverage_ci.py - CI Coverage Analysis
import subprocess
import sys
import json
import os
def run_coverage():
"""Run coverage with CI-friendly output"""
# Run tests with coverage
cmd = [
'pytest',
'--cov=src',
'--cov-branch',
'--cov-report=json',
'--cov-report=xml',
'--cov-report=term',
'--cov-fail-under=80',
'--junitxml=test-results.xml'
]
result = subprocess.run(cmd, capture_output=True, text=True)
print("STDOUT:")
print(result.stdout)
if result.stderr:
print("STDERR:")
print(result.stderr)
# Parse coverage results
if os.path.exists('coverage.json'):
with open('coverage.json', 'r') as f:
coverage_data = json.load(f)
total_coverage = coverage_data['totals']['percent_covered']
print(f"\n{'='*50}")
print(f"COVERAGE SUMMARY")
print(f"{'='*50}")
print(f"Total Coverage: {total_coverage:.2f}%")
print(f"Covered Lines: {coverage_data['totals']['covered_lines']}")
print(f"Missing Lines: {coverage_data['totals']['missing_lines']}")
print(f"Total Lines: {coverage_data['totals']['num_statements']}")
if 'covered_branches' in coverage_data['totals']:
print(f"Branch Coverage: {coverage_data['totals']['percent_covered_display']}%")
# Set environment variables for CI
with open(os.environ.get('GITHUB_ENV', '/dev/null'), 'a') as env_file:
env_file.write(f"COVERAGE_PERCENTAGE={total_coverage:.2f}\n")
# Exit with error if coverage too low
if total_coverage < 80:
print(f"\n❌ Coverage {total_coverage:.2f}% is below required 80%")
sys.exit(1)
else:
print(f"\n✅ Coverage {total_coverage:.2f}% meets requirements")
return result.returncode
if __name__ == "__main__":
sys.exit(run_coverage())
# Dockerfile for coverage testing
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Install test dependencies
RUN pip install pytest pytest-cov coverage
# Copy source code
COPY src/ ./src/
COPY tests/ ./tests/
COPY .coveragerc ./
# Run coverage
CMD ["python", "-m", "pytest", "--cov=src", "--cov-report=html", "--cov-report=xml", "--cov-report=term-missing"]