import os
import subprocess
import xml.etree.ElementTree as ET
def get_diff_added_lines():
"""获取PR中新增的代码行,并输出调试信息"""
diff_cmd = ["git", "diff", "origin/master...HEAD", "--unified=0", "--", ":!test_*"]
print(f"Running command: {' '.join(diff_cmd)}")
diff_output = subprocess.run(diff_cmd, capture_output=True, text=True).stdout
added_lines = {}
current_file = None
for line in diff_output.splitlines():
if line.startswith("+++ b/"):
current_file = line[6:]
if "test/" in current_file or current_file.startswith("test_") or current_file.endswith(".md"):
print(f"Ignoring test file: {current_file}")
current_file = None
continue
added_lines[current_file] = []
print(f"Detected new file: {current_file}")
elif line.startswith("@@") and current_file is not None:
hunk_info = line.split()[2]
added_start_line = int(hunk_info.split(",")[0][1:])
line_count = int(hunk_info.split(",")[1]) if "," in hunk_info else 1
added_lines[current_file].extend(range(added_start_line, added_start_line + line_count))
return added_lines
def parse_coverage(coverage_file):
"""解析coverage.xml并提取覆盖的行,并输出调试信息"""
print(f"Parsing coverage file: {coverage_file}")
if not os.path.exists(coverage_file):
print(f"Coverage file {coverage_file} does not exist!")
return {}
tree = ET.parse(coverage_file)
root = tree.getroot()
covered_lines = {}
for file_elem in root.findall(".//class"):
filename = file_elem.attrib['filename']
lines = {"covered": [], "uncovered": []}
for line_elem in file_elem.findall(".//line"):
line_num = int(line_elem.attrib['number'])
if line_elem.attrib['hits'] == "0":
lines["uncovered"].append(line_num)
else:
lines["covered"].append(line_num)
covered_lines[filename] = lines
return covered_lines
def normalize_path(file_path, keep_parts=2):
"""
移除路径的前缀,只保留文件名的相对部分。
:param file_path: 原始文件路径
:param keep_parts: 需要保留的路径部分数(从后向前数)
:return: 标准化并截取后的路径
"""
normalized_path = os.path.normpath(file_path)
path_parts = normalized_path.split(os.sep)
return os.path.join(*path_parts[-keep_parts:])
def calculate_coverage(added_lines, covered_lines):
"""计算新增代码的覆盖率,并输出调试信息"""
total_added = 0
total_covered = 0
total_annotation = 0
for filename, lines in added_lines.items():
normalized_filename = normalize_path(filename)
print(f"normalized_filename:{normalized_filename}")
found = False
for covered_file in covered_lines:
if normalize_path(covered_file).endswith(normalized_filename):
found = True
print(f"Processing file: {filename}")
covered_file_lines = covered_lines[covered_file]["covered"]
uncovered_file_lines = covered_lines[covered_file]["uncovered"]
for line in lines:
total_added += 1
if line in covered_file_lines:
total_covered += 1
print(f"Line {line} in {filename} is covered.")
elif line in uncovered_file_lines:
pass
print(f"Line {line} in {filename} is NOT covered.")
else:
total_annotation += 1
print(f"Line {line} in {filename} is not present in coverage report.")
break
if not found:
print(f"File {filename} not found in coverage report.")
total_effective_lines = total_added - total_annotation
print(f"Total added lines: {total_added}")
print(f"Total annotation lines: {total_annotation}")
print(f"Total effective lines: {total_effective_lines}")
print(f"Total covered lines: {total_covered}")
coverage_rate = (total_covered / total_effective_lines * 100) if total_effective_lines > 0 else 0
return coverage_rate
if __name__ == "__main__":
added_lines = get_diff_added_lines()
cur_dir = os.path.realpath(os.path.dirname(__file__))
coverage_file = os.path.join(cur_dir, "report", "coverage.xml")
covered_lines = parse_coverage(coverage_file)
coverage_rate = calculate_coverage(added_lines, covered_lines)
print(f"Coverage rate of added code: {coverage_rate:.2f}%")