Skip to main content

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"]