Skip to content

Commit cd6461c

Browse files
committed
Update for sending metrics report to bitbucket
1 parent aecb100 commit cd6461c

File tree

3 files changed

+418
-59
lines changed

3 files changed

+418
-59
lines changed

generate_metrics_md.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#
2+
# The MIT License
3+
#
4+
# Copyright 2025 Vector Informatik, GmbH.
5+
#
6+
# Permission is hereby granted, free of charge, to any person obtaining a copy
7+
# of this software and associated documentation files (the "Software"), to deal
8+
# in the Software without restriction, including without limitation the rights
9+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
# copies of the Software, and to permit persons to whom the Software is
11+
# furnished to do so, subject to the following conditions:
12+
#
13+
# The above copyright notice and this permission notice shall be included in
14+
# all copies or substantial portions of the Software.
15+
#
16+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
# THE SOFTWARE.
23+
#
24+
25+
import sys, io, re, os
26+
from vector.apps.ReportBuilder.custom_report import CustomReport
27+
from vector.apps.DataAPI.vcproject_api import VCProjectApi
28+
29+
# ----------------------------------------------------------------------
30+
# Emoji setup (2.7 + 3.x safe)
31+
PASS = u"\u2705"
32+
FAIL = u"\u274C"
33+
PARTIAL = u"\U0001F7E1"
34+
PART = PARTIAL
35+
36+
def updateTextMetricsReport(mpReportName):
37+
"""
38+
Parse a VectorCAST metrics report (.txt) and return:
39+
summary -> Overall Coverage string
40+
annotations -> list of [file, summary string, severity]
41+
"""
42+
encFmt = "utf-8"
43+
COV_RE = re.compile(r"\(([\d.]+)%\)")
44+
TOTALS_RE = re.compile(r"^\s*(?:GRAND\s+)?TOTALS\b", re.IGNORECASE)
45+
46+
def emoji_for(pct):
47+
"""Return (emoji, weight) where weight contributes to severity."""
48+
try:
49+
p = float(pct)
50+
except Exception:
51+
return u"", 0
52+
53+
if p >= 100.0:
54+
return PASS, -1 # perfect -> bonus credit
55+
elif p >= 80.0:
56+
return PASS, 0 # solid
57+
elif p >= 50.0:
58+
return PART, 1 # fair
59+
elif p > 0.0:
60+
return PART, 2 # weak
61+
else:
62+
return FAIL, 3 # failed
63+
64+
def severity_for(emoji_pairs):
65+
"""Compute overall severity using average weighted score."""
66+
weights = [w for (_, w) in emoji_pairs if w is not None]
67+
if not weights:
68+
return "LOW"
69+
70+
avg = sum(weights) / float(len(weights))
71+
72+
if avg <= 0: # many perfect or high scores
73+
return "LOW"
74+
elif avg <= 0.75: # mostly PASS with some PART
75+
return "MEDIUM"
76+
elif avg <= 1.75: # mix of PASS/PART and a few FAIL
77+
return "HIGH"
78+
else: # mostly FAIL
79+
return "CRITICAL"
80+
81+
# --- find the metrics text file ---
82+
if not os.path.exists(mpReportName):
83+
raise FileNotFoundError("Cannot find metrics file: {}".format(mpReportName))
84+
85+
# --- read and parse ---
86+
lines = []
87+
with open(mpReportName, "rb") as fd:
88+
lines = [line.decode(encFmt, "replace") for line in fd.readlines()]
89+
90+
rows = []
91+
grand_total = None
92+
93+
for line in lines:
94+
if not line.strip() or line.strip().startswith("-"):
95+
continue
96+
if line.upper().strip().startswith("GRAND TOTALS"):
97+
grand_total = line
98+
continue
99+
100+
# file total lines contain '.c' or '.cpp' and percentages
101+
if (".c" in line or ".cpp" in line) and "(" in line:
102+
parts = re.split(r"\s{2,}", line.strip())
103+
file_ = parts[0]
104+
105+
# Extract numeric percentages
106+
pcts = [float(m.group(1)) for m in COV_RE.finditer(line)]
107+
108+
# Map percentages to (emoji, weight)
109+
emoji_pairs = [emoji_for(p) for p in pcts[:5]]
110+
emojis = [e for (e, _) in emoji_pairs]
111+
112+
# Pad out to 5 entries if fewer found
113+
while len(emojis) < 5:
114+
emojis.append(u"")
115+
116+
# Compute severity from weighted averages
117+
severity = severity_for(emoji_pairs)
118+
119+
# Build markdown coverage summary line
120+
coverage_str = u"FN {0} | ST {1} | BR {2} | PR {3} | FC {4}".format(
121+
emojis[0], emojis[1], emojis[2], emojis[3], emojis[4])
122+
123+
# Store result
124+
rows.append([file_, coverage_str, severity])
125+
126+
# --- build summary (from GRAND TOTALS line) ---
127+
summary = u""
128+
129+
if grand_total:
130+
pcts = [float(m.group(1)) for m in COV_RE.finditer(grand_total)]
131+
132+
# get (emoji, weight) pairs, extract emoji only
133+
emoji_pairs = [emoji_for(p) for p in pcts[:5]]
134+
emojis = [e for (e, _) in emoji_pairs]
135+
136+
while len(emojis) < 5:
137+
emojis.append(u"")
138+
139+
summary = u"Overall Coverage: FN {0} | ST {1} | BR {2} | PR {3} | FC {4}".format(
140+
emojis[0], emojis[1], emojis[2], emojis[3], emojis[4])
141+
142+
# --- build Markdown output ---
143+
md_lines = [u"## Summary", summary, u"\n## Annotations",
144+
u"| File | Summary | Severity |",
145+
u"|------|----------|-----------|"]
146+
147+
for file_, cov, sev in rows:
148+
md_lines.append(u"| {0} | {1} | {2} |".format(file_, cov, sev))
149+
150+
md_text = u"\n".join(md_lines)
151+
md_path = os.path.splitext(mpReportName)[0] + "_summary.md"
152+
153+
with io.open(md_path, "w", encoding=encFmt) as out:
154+
out.write(md_text + u"\n")
155+
156+
print(u"Markdown written to {}".format(md_path))
157+
158+
artifactName = mpReportName.replace("_metrics_report.txt","_aggregate_report.html")
159+
160+
workspace = os.environ['BITBUCKET_WORKSPACE']
161+
repo_slug = os.environ['BITBUCKET_REPO_SLUG']
162+
build_num = os.environ['BITBUCKET_BUILD_NUMBER']
163+
164+
# --- Append link to full HTML report ---
165+
html_artifact_url = "https://bitbucket.org/{}/{}/addons/bitbucket-build/{}/artifacts/reports/html/{}".format(workspace, repo_slug, build_num, artifactName)
166+
167+
link = {"text": "Aggregate Coverage Report", "href": html_artifact_url}
168+
169+
return summary, rows, link
170+
171+
def generate_metrics_md(mpName):
172+
173+
print("Generating metrics to BitBucket in Markdown format")
174+
mpBaseName = os.path.basename(mpName)[:-4]
175+
report_name = "{}_metrics_report.txt".format(mpBaseName)
176+
177+
with VCProjectApi(mpName) as vcproj:
178+
CustomReport.report_from_api(vcproj, report_type="Demo", formats=["TEXT"], output_file=report_name, sections=["METRICS"])
179+
180+
summary, rows, link = updateTextMetricsReport(report_name)
181+
182+
return summary, rows, link
183+
184+
if __name__ == '__main__':
185+
import argparse
186+
parser = argparse.ArgumentParser()
187+
parser.add_argument('manageProject' , help='VectorCAST Project name')
188+
args = parser.parse_args()
189+
190+
if not args.manageProject.endswith(".vcm"):
191+
args.manageProject += ".vcm"
192+
193+
if not os.path.exists(args.manageProject):
194+
print ("VectorCAST Project not found! " + args.manageProject)
195+
sys.exit(-1)
196+
197+
summary, rows = generate_metrics_md(args.manageProject)

0 commit comments

Comments
 (0)