mirror of
				https://github.com/Ukendio/jecs.git
				synced 2025-11-04 02:49:18 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			153 lines
		
	
	
	
		
			6.7 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
			
		
		
	
	
			153 lines
		
	
	
	
		
			6.7 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
import os
 | 
						|
 | 
						|
LCOV_FILE = "coverage.out"
 | 
						|
OUTPUT_DIR = "coverage"
 | 
						|
 | 
						|
os.makedirs(OUTPUT_DIR, exist_ok=True)
 | 
						|
 | 
						|
def parse_lcov(content):
 | 
						|
    """Parses LCOV data from a single string."""
 | 
						|
    files = {}
 | 
						|
    current_file = None
 | 
						|
 | 
						|
    for line in content.splitlines():
 | 
						|
        if line.startswith("SF:"):
 | 
						|
            current_file = line[3:].strip()
 | 
						|
            files[current_file] = {"coverage": {}, "functions": []}
 | 
						|
        elif line.startswith("DA:") and current_file:
 | 
						|
            parts = line[3:].split(",")
 | 
						|
            line_num = int(parts[0])
 | 
						|
            execution_count = int(parts[1])
 | 
						|
            files[current_file]["coverage"][line_num] = execution_count
 | 
						|
        elif line.startswith("FN:") and current_file:
 | 
						|
            parts = line[3:].split(",")
 | 
						|
            line_num = int(parts[0])
 | 
						|
            function_name = parts[1].strip()
 | 
						|
            files[current_file]["functions"].append({"name": function_name, "line": line_num, "hits": 0})
 | 
						|
        elif line.startswith("FNDA:") and current_file:
 | 
						|
            parts = line[5:].split(",")
 | 
						|
            hit_count = int(parts[0])
 | 
						|
            function_name = parts[1].strip()
 | 
						|
            for func in files[current_file]["functions"]:
 | 
						|
                if func["name"] == function_name:
 | 
						|
                    func["hits"] = hit_count
 | 
						|
                    break
 | 
						|
 | 
						|
    return files
 | 
						|
 | 
						|
def read_source_file(filepath):
 | 
						|
    """Reads source file content if available."""
 | 
						|
    if not os.path.exists(filepath):
 | 
						|
        return []
 | 
						|
 | 
						|
    with open(filepath, "r", encoding="utf-8") as f:
 | 
						|
        return f.readlines()
 | 
						|
 | 
						|
def generate_file_html(filepath, coverage_data, functions_data):
 | 
						|
    """Generates an HTML file for a specific source file."""
 | 
						|
    filename = os.path.basename(filepath)
 | 
						|
    source_code = read_source_file(filepath)
 | 
						|
    html_path = os.path.join(OUTPUT_DIR, f"{filename}.html")
 | 
						|
 | 
						|
    total_hits = sum(func["hits"] for func in functions_data)
 | 
						|
    max_hits = max((func["hits"] for func in functions_data), default=0)
 | 
						|
 | 
						|
    total_functions = len(functions_data)
 | 
						|
    covered_functions = sum(1 for func in functions_data if func["hits"] > 0)
 | 
						|
    function_coverage_percent = (covered_functions / total_functions * 100) if total_functions > 0 else 0
 | 
						|
 | 
						|
    lines = [
 | 
						|
        "<html><head>",
 | 
						|
        '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css">',
 | 
						|
        '<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>',
 | 
						|
        "<style>",
 | 
						|
        "body { font-family: monospace; text-align: center; }",
 | 
						|
        "#funcTable table { margin: 0 auto; width: auto; max-width: 300px; font-size: 14px; border-collapse: collapse; }",
 | 
						|
        "#funcTable th, #funcTable td { padding: 2px 6px; text-align: left; white-space: nowrap; }",
 | 
						|
        "#funcTable th { background-color: #ddd; }",
 | 
						|
        "#funcTable td:nth-child(2) { text-align: right; min-width: 50px; }",
 | 
						|
        ".zero-hits { background-color: #fcc; font-weight: bold; color: red; }",
 | 
						|
        ".nonzero-hits { color: green; font-weight: bold; }",
 | 
						|
        ".low-hits { background-color: #ffe6b3; }",
 | 
						|
        ".high-hits { background-color: #cfc; }",
 | 
						|
        ".source-code-table { margin-left: 10px; }"
 | 
						|
        "th, td { padding: 0px; font-size: 12px; }",
 | 
						|
        "table.table { font-size: 14px; border-collapse: collapse; }",
 | 
						|
        "table.table th, table.table td { padding: 1px; font-size: 12px; line-height: 1.2; }",
 | 
						|
        "table.table tr { height: auto; }",
 | 
						|
        "</style></head><body>",
 | 
						|
        f'<h1 class="text-center">{filename} Coverage</h1>',
 | 
						|
        f'<h2>Total Execution Hits: {total_hits}</h2>',
 | 
						|
        f'<h2>Function Coverage Overview: {function_coverage_percent:.2f}%</h2>',
 | 
						|
 | 
						|
        '<button class="btn btn-primary mb-2" type="button" data-bs-toggle="collapse" data-bs-target="#funcTable">'
 | 
						|
        'Toggle Function Coverage</button>',
 | 
						|
 | 
						|
        '<div class="collapse show" id="funcTable">',
 | 
						|
        '<h2>Function Coverage:</h2><table class="table table-bordered"><thead><tr><th>Function</th><th>Hits</th></tr></thead><tbody>'
 | 
						|
    ]
 | 
						|
 | 
						|
    longest_name = max((len(func["name"]) for func in functions_data), default=0)
 | 
						|
 | 
						|
    for func in functions_data:
 | 
						|
        hit_color = "red" if func["hits"] == 0 else "green"
 | 
						|
        lines.append(
 | 
						|
            f'<tr><td style="padding: 1px; min-width: {longest_name}ch;">{func["name"]}</td>'
 | 
						|
            f'<td style="padding: 1px; color: {hit_color}; font-weight: bold;">{func["hits"]}</td></tr>'
 | 
						|
        )
 | 
						|
 | 
						|
    lines.append('</tbody></table></div>')  # Close collapsible div
 | 
						|
 | 
						|
    lines.append('<h2>Source Code:</h2><table class="table table-bordered source-code-table "><thead><tr><th>Line</th><th>Hits</th><th>Code</th></tr></thead><tbody>')
 | 
						|
 | 
						|
    for i, line in enumerate(source_code, start=1):
 | 
						|
        stripped_line = line.strip()
 | 
						|
        class_name = "text-muted"
 | 
						|
        if not stripped_line or stripped_line.startswith("end") or stripped_line.startswith("--"):
 | 
						|
            count_display = "<span class='text-muted'>N/A</span>"
 | 
						|
            lines.append(f'<tr><td>{i}</td><td>{count_display}</td><td>{line.strip()}</td>></tr>')
 | 
						|
        else:
 | 
						|
            count = coverage_data.get(i, 0)
 | 
						|
            class_name = "zero-hits" if count == 0 else "low-hits" if count < max_hits * 0.3 else "high-hits"
 | 
						|
            count_display = f'{count}'
 | 
						|
            marked_text = f'<span class={class_name}>{line.strip()}</span>'
 | 
						|
            lines.append(f'<tr><td>{i}</td><td>{count_display}</td><td>{marked_text}</td></tr>')
 | 
						|
 | 
						|
    lines.append("</tbody></table></body></html>")
 | 
						|
 | 
						|
    with open(html_path, "w", encoding="utf-8") as f:
 | 
						|
        f.write("\n".join(lines))
 | 
						|
 | 
						|
def generate_index(files):
 | 
						|
    """Generates an index.html summarizing the coverage."""
 | 
						|
    index_html = [
 | 
						|
        "<html><head>",
 | 
						|
        '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css">',
 | 
						|
        "</head><body>",
 | 
						|
        '<h1 class="text-center">Coverage Report</h1>',
 | 
						|
        '<table class="table table-striped table-bordered"><thead><tr><th>File</th><th>Total Hits</th><th>Functions</th></tr></thead><tbody>'
 | 
						|
    ]
 | 
						|
 | 
						|
    for filepath, data in files.items():
 | 
						|
        filename = os.path.basename(filepath)
 | 
						|
        total_hits = sum(func["hits"] for func in data["functions"])
 | 
						|
        total_functions = len(data["functions"])
 | 
						|
 | 
						|
        index_html.append(f'<tr><td><a href="{filename}.html">{filename}</a></td><td>{total_hits}</td><td>{total_functions}</td></tr>')
 | 
						|
 | 
						|
    index_html.append("</tbody></table></body></html>")
 | 
						|
 | 
						|
    with open(os.path.join(OUTPUT_DIR, "index.html"), "w", encoding="utf-8") as f:
 | 
						|
        f.write("\n".join(index_html))
 | 
						|
 | 
						|
with open(LCOV_FILE, "r", encoding="utf-8") as f:
 | 
						|
    lcov_content = f.read()
 | 
						|
 | 
						|
files_data = parse_lcov(lcov_content)
 | 
						|
 | 
						|
for file_path, data in files_data.items():
 | 
						|
    generate_file_html(file_path, data["coverage"], data["functions"])
 | 
						|
 | 
						|
generate_index(files_data)
 | 
						|
 | 
						|
print(f"Coverage report generated in {OUTPUT_DIR}/index.html")
 |