370 lines
13 KiB
Python
370 lines
13 KiB
Python
"""Generate formatted text reports for differ results."""
|
|
|
|
import datetime
|
|
import os
|
|
|
|
|
|
def generate_differ_report(result, profile_config, baseline_id, output_path):
|
|
"""
|
|
Generate a well-formatted text report for differ results.
|
|
|
|
Args:
|
|
result: dict with differ results (from _current_results)
|
|
profile_config: dict with profile settings (paths, languages, exclude_patterns)
|
|
baseline_id: str, baseline identifier
|
|
output_path: str, path where to save the report
|
|
"""
|
|
lines = []
|
|
|
|
# Header
|
|
lines.append("=" * 80)
|
|
lines.append("PyUcc - Code Difference Report".center(80))
|
|
lines.append("=" * 80)
|
|
lines.append("")
|
|
|
|
# Timestamp
|
|
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
lines.append(f"Generated: {timestamp}")
|
|
lines.append(f"Baseline ID: {baseline_id}")
|
|
lines.append("")
|
|
|
|
# Profile Configuration
|
|
lines.append("-" * 80)
|
|
lines.append("Profile Configuration")
|
|
lines.append("-" * 80)
|
|
lines.append(f"Profile Name: {profile_config.get('name', 'N/A')}")
|
|
lines.append(f"Project Root: {profile_config.get('root', 'N/A')}")
|
|
lines.append("")
|
|
|
|
paths = profile_config.get("paths", [])
|
|
if paths:
|
|
lines.append(f"Included Paths ({len(paths)}):")
|
|
for p in paths:
|
|
lines.append(f" - {p}")
|
|
else:
|
|
lines.append("Included Paths: All")
|
|
lines.append("")
|
|
|
|
languages = profile_config.get("languages", [])
|
|
if languages:
|
|
lines.append(f"Languages ({len(languages)}):")
|
|
lang_str = ", ".join(languages)
|
|
# Wrap at 70 chars
|
|
if len(lang_str) > 70:
|
|
lines.append(f" {lang_str[:70]}")
|
|
lines.append(f" {lang_str[70:]}")
|
|
else:
|
|
lines.append(f" {lang_str}")
|
|
else:
|
|
lines.append("Languages: All")
|
|
lines.append("")
|
|
|
|
exclude_patterns = profile_config.get("exclude_patterns", [])
|
|
if exclude_patterns:
|
|
lines.append(f"Exclude Patterns ({len(exclude_patterns)}):")
|
|
for pat in exclude_patterns:
|
|
lines.append(f" - {pat}")
|
|
else:
|
|
lines.append("Exclude Patterns: None")
|
|
lines.append("")
|
|
|
|
# Summary Statistics
|
|
lines.append("-" * 80)
|
|
lines.append("Summary Statistics")
|
|
lines.append("-" * 80)
|
|
|
|
total = result.get("total", {})
|
|
lines.append(f"Files Added: {total.get('added', 0):>8}")
|
|
lines.append(f"Files Deleted: {total.get('deleted', 0):>8}")
|
|
lines.append(f"Files Modified: {total.get('modified', 0):>8}")
|
|
lines.append(f"Files Unmodified: {total.get('unmodified', 0):>8}")
|
|
lines.append(f"{'─' * 30}")
|
|
total_files = sum(
|
|
[
|
|
total.get("added", 0),
|
|
total.get("deleted", 0),
|
|
total.get("modified", 0),
|
|
total.get("unmodified", 0),
|
|
]
|
|
)
|
|
lines.append(f"Total Files: {total_files:>8}")
|
|
lines.append("")
|
|
|
|
# Baseline vs Current metrics
|
|
summary = result.get("summary", {})
|
|
if summary:
|
|
baseline_counts = summary.get("baseline", {}).get("countings", {})
|
|
current_counts = summary.get("current", {}).get("countings", {})
|
|
baseline_metrics = summary.get("baseline", {}).get("metrics", {})
|
|
current_metrics = summary.get("current", {}).get("metrics", {})
|
|
|
|
lines.append("-" * 80)
|
|
lines.append("Code Metrics Comparison")
|
|
lines.append("-" * 80)
|
|
lines.append(f"{'Metric':<30} {'Baseline':>15} {'Current':>15} {'Delta':>15}")
|
|
lines.append("─" * 80)
|
|
|
|
# Countings
|
|
def _delta_str(baseline_val, current_val):
|
|
delta = current_val - baseline_val
|
|
sign = "+" if delta >= 0 else ""
|
|
return f"{sign}{delta}"
|
|
|
|
bc_code = baseline_counts.get("code_lines", 0)
|
|
cc_code = current_counts.get("code_lines", 0)
|
|
lines.append(
|
|
f"{'Code Lines':<30} {bc_code:>15,} {cc_code:>15,} {_delta_str(bc_code, cc_code):>15}"
|
|
)
|
|
|
|
bc_comment = baseline_counts.get("comment_lines", 0)
|
|
cc_comment = current_counts.get("comment_lines", 0)
|
|
lines.append(
|
|
f"{'Comment Lines':<30} {bc_comment:>15,} {cc_comment:>15,} {_delta_str(bc_comment, cc_comment):>15}"
|
|
)
|
|
|
|
bc_blank = baseline_counts.get("blank_lines", 0)
|
|
cc_blank = current_counts.get("blank_lines", 0)
|
|
lines.append(
|
|
f"{'Blank Lines':<30} {bc_blank:>15,} {cc_blank:>15,} {_delta_str(bc_blank, cc_blank):>15}"
|
|
)
|
|
|
|
bc_physical = baseline_counts.get("physical_lines", 0)
|
|
cc_physical = current_counts.get("physical_lines", 0)
|
|
lines.append(
|
|
f"{'Physical Lines':<30} {bc_physical:>15,} {cc_physical:>15,} {_delta_str(bc_physical, cc_physical):>15}"
|
|
)
|
|
|
|
lines.append("")
|
|
|
|
# Metrics
|
|
bm_func = baseline_metrics.get("total_func_count", 0)
|
|
cm_func = current_metrics.get("total_func_count", 0)
|
|
lines.append(
|
|
f"{'Function Count':<30} {bm_func:>15,} {cm_func:>15,} {_delta_str(bm_func, cm_func):>15}"
|
|
)
|
|
|
|
bm_avgcc = baseline_metrics.get("avg_avg_cc", 0.0)
|
|
cm_avgcc = current_metrics.get("avg_avg_cc", 0.0)
|
|
lines.append(
|
|
f"{'Avg Cyclomatic Complexity':<30} {bm_avgcc:>15.2f} {cm_avgcc:>15.2f} {_delta_str(bm_avgcc, cm_avgcc):>15}"
|
|
)
|
|
|
|
bm_mi = baseline_metrics.get("avg_mi", 0.0)
|
|
cm_mi = current_metrics.get("avg_mi", 0.0)
|
|
lines.append(
|
|
f"{'Maintainability Index':<30} {bm_mi:>15.2f} {cm_mi:>15.2f} {_delta_str(bm_mi, cm_mi):>15}"
|
|
)
|
|
|
|
lines.append("")
|
|
|
|
# Detailed File List
|
|
lines.append("-" * 80)
|
|
lines.append("Detailed File Changes")
|
|
lines.append("-" * 80)
|
|
lines.append("")
|
|
|
|
pairs = result.get("pairs", [])
|
|
|
|
# Group by status
|
|
added_files = []
|
|
deleted_files = []
|
|
modified_files = []
|
|
unmodified_files = []
|
|
|
|
for pair in pairs:
|
|
counts = pair.get("counts", {})
|
|
if counts.get("added", 0) > 0:
|
|
added_files.append(pair)
|
|
elif counts.get("deleted", 0) > 0:
|
|
deleted_files.append(pair)
|
|
elif counts.get("modified", 0) > 0:
|
|
modified_files.append(pair)
|
|
else:
|
|
unmodified_files.append(pair)
|
|
|
|
# Added files
|
|
if added_files:
|
|
lines.append(f"Added Files ({len(added_files)}):")
|
|
lines.append("─" * 80)
|
|
for pair in added_files:
|
|
current_name = pair.get("fileB", "")
|
|
cd = pair.get("countings_delta")
|
|
md = pair.get("metrics_delta")
|
|
cur_m = pair.get("current_metrics")
|
|
|
|
lines.append(f" + {current_name}")
|
|
if cd:
|
|
lines.append(
|
|
f" Code: {cd.get('code_lines', 0):>6} Comment: {cd.get('comment_lines', 0):>6} Blank: {cd.get('blank_lines', 0):>6}"
|
|
)
|
|
if cur_m:
|
|
lines.append(
|
|
f" Functions: {cur_m.get('func_count', 0):>4} Avg CC: {cur_m.get('avg_cc', 0.0):>6.2f} MI: {cur_m.get('mi', 0.0):>6.2f}"
|
|
)
|
|
lines.append("")
|
|
|
|
# Deleted files
|
|
if deleted_files:
|
|
lines.append(f"Deleted Files ({len(deleted_files)}):")
|
|
lines.append("─" * 80)
|
|
for pair in deleted_files:
|
|
baseline_name = pair.get("fileA", "")
|
|
lines.append(f" - {baseline_name}")
|
|
lines.append("")
|
|
|
|
# Modified files
|
|
if modified_files:
|
|
lines.append(f"Modified Files ({len(modified_files)}):")
|
|
lines.append("─" * 80)
|
|
for pair in modified_files:
|
|
current_name = pair.get("fileB", "")
|
|
baseline_name = pair.get("fileA", "")
|
|
cd = pair.get("countings_delta")
|
|
md = pair.get("metrics_delta")
|
|
|
|
lines.append(f" M {current_name}")
|
|
if baseline_name != current_name:
|
|
lines.append(f" (was: {baseline_name})")
|
|
|
|
# Always show countings delta, even if all zeros (file was modified at byte level)
|
|
if cd:
|
|
delta_code = cd.get("code_lines", 0)
|
|
delta_comment = cd.get("comment_lines", 0)
|
|
delta_blank = cd.get("blank_lines", 0)
|
|
|
|
# Check if there are any non-zero deltas
|
|
has_counting_changes = any(
|
|
[delta_code != 0, delta_comment != 0, delta_blank != 0]
|
|
)
|
|
|
|
if has_counting_changes:
|
|
sign_code = "+" if delta_code >= 0 else ""
|
|
sign_comment = "+" if delta_comment >= 0 else ""
|
|
sign_blank = "+" if delta_blank >= 0 else ""
|
|
lines.append(
|
|
f" Code: {sign_code}{delta_code:>6} Comment: {sign_comment}{delta_comment:>6} Blank: {sign_blank}{delta_blank:>6}"
|
|
)
|
|
else:
|
|
lines.append(
|
|
f" Code: + 0 Comment: + 0 Blank: + 0"
|
|
)
|
|
|
|
# Always show metrics delta, even if all zeros
|
|
if md:
|
|
delta_func = md.get("func_count", 0)
|
|
delta_avgcc = md.get("avg_cc", 0.0)
|
|
delta_mi = md.get("mi", 0.0)
|
|
|
|
# Check if there are any non-zero deltas
|
|
has_metric_changes = any(
|
|
[delta_func != 0, abs(delta_avgcc) > 0.01, abs(delta_mi) > 0.01]
|
|
)
|
|
|
|
if has_metric_changes:
|
|
sign_func = "+" if delta_func >= 0 else ""
|
|
sign_avgcc = "+" if delta_avgcc >= 0 else ""
|
|
sign_mi = "+" if delta_mi >= 0 else ""
|
|
lines.append(
|
|
f" Functions: {sign_func}{delta_func:>4} Avg CC: {sign_avgcc}{delta_avgcc:>6.2f} MI: {sign_mi}{delta_mi:>6.2f}"
|
|
)
|
|
else:
|
|
lines.append(
|
|
f" Functions: + 0 Avg CC: + 0.00 MI: + 0.00"
|
|
)
|
|
lines.append("")
|
|
|
|
# Sort pairs by current file name for consistent ordering
|
|
sorted_pairs = sorted(
|
|
pairs, key=lambda p: p.get("fileB", p.get("fileA", "")).lower()
|
|
)
|
|
|
|
# Also generate a compact UCC-style table (New | Deleted | Modified | Unmodified | Module)
|
|
# with extra delta columns appended for richer information.
|
|
# Insert an English legend explaining the table columns so the reader understands
|
|
# each field before the compact table is shown.
|
|
lines.append("")
|
|
lines.append("Legend of table fields:")
|
|
lines.append(
|
|
" New : Number of lines added in this file compared to the baseline"
|
|
)
|
|
lines.append(" Del : Number of lines deleted compared to the baseline")
|
|
lines.append(
|
|
" Mod : Number of lines modified (replaced) compared to the baseline"
|
|
)
|
|
lines.append(" Unmod : Number of lines unchanged")
|
|
lines.append(
|
|
" St : File status (A=Added, D=Deleted, M=Modified, = = Unmodified)"
|
|
)
|
|
lines.append(" ΔCode : Change in code lines (current - baseline)")
|
|
lines.append(" ΔComm : Change in comment lines (current - baseline)")
|
|
lines.append(" ΔBlank : Change in blank lines (current - baseline)")
|
|
lines.append(" ΔFunc : Change in number of functions (current - baseline)")
|
|
lines.append(" ΔAvgCC : Change in average cyclomatic complexity per function")
|
|
lines.append(" ΔMI : Change in Maintainability Index (MI)")
|
|
lines.append("")
|
|
lines.append("UCC-Style Compact Table:")
|
|
# Header with fixed column widths for good alignment
|
|
header_cols = f"{'New':>3} | {'Del':>3} | {'Mod':>3} | {'Unmod':>7} | {'St':^3} | {'ΔCode':>6} | {'ΔComm':>6} | {'ΔBlank':>6} | {'ΔFunc':>5} | {'ΔAvgCC':>7} | {'ΔMI':>6} | Module"
|
|
lines.append(header_cols)
|
|
lines.append("-" * 140)
|
|
|
|
# Use the same sorted_pairs order
|
|
for pair in sorted_pairs:
|
|
counts = pair.get("counts", {})
|
|
a = counts.get("added", 0)
|
|
d = counts.get("deleted", 0)
|
|
m = counts.get("modified", 0)
|
|
u = counts.get("unmodified", 0)
|
|
|
|
cd = pair.get("countings_delta") or {}
|
|
md = pair.get("metrics_delta") or {}
|
|
|
|
def fmt_int(v):
|
|
try:
|
|
return str(int(v))
|
|
except Exception:
|
|
return "0"
|
|
|
|
def fmt_float(v):
|
|
try:
|
|
return f"{float(v):.2f}"
|
|
except Exception:
|
|
return "0.00"
|
|
|
|
row_mod = pair.get("fileB") or pair.get("fileA") or ""
|
|
# Trim long module names similarly to UCC display
|
|
if len(row_mod) > 80:
|
|
row_mod = ".." + row_mod[-78:]
|
|
# Determine status char similar to other parts of the report
|
|
if a > 0:
|
|
st = "A"
|
|
elif d > 0:
|
|
st = "D"
|
|
elif m > 0:
|
|
st = "M"
|
|
else:
|
|
st = "="
|
|
|
|
# Compose a well-aligned row using the same widths as the header
|
|
line = (
|
|
f"{a:>3} | {d:>3} | {m:>3} | {u:>7} | {st:^3} | "
|
|
f"{fmt_int(cd.get('code_lines')):>6} | {fmt_int(cd.get('comment_lines')):>6} | {fmt_int(cd.get('blank_lines')):>6} | {fmt_int(md.get('func_count')):>5} | {fmt_float(md.get('avg_cc')):>7} | {fmt_float(md.get('mi')):>6} | {row_mod}"
|
|
)
|
|
lines.append(line)
|
|
|
|
lines.append("")
|
|
|
|
# Footer
|
|
lines.append("=" * 80)
|
|
lines.append("End of Report".center(80))
|
|
lines.append("=" * 80)
|
|
|
|
# Write to file
|
|
try:
|
|
with open(output_path, "w", encoding="utf-8") as f:
|
|
f.write("\n".join(lines))
|
|
return True
|
|
except Exception as e:
|
|
print(f"Failed to write report: {e}")
|
|
return False
|