Skip to content

Commit 3239ef4

Browse files
Merge pull request #2714 from eggfoobar/upkeep-updated-feature-verify-html
NO-JIRA: feat: update the html for feature promotion
2 parents 4528fd0 + 9650576 commit 3239ef4

2 files changed

Lines changed: 254 additions & 26 deletions

File tree

tools/codegen/cmd/featuregate-test-analyzer.go

Lines changed: 88 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"errors"
88
"fmt"
9+
"html/template"
910
"io"
1011
"net/http"
1112
"net/url"
@@ -16,7 +17,6 @@ import (
1617
"strings"
1718
"time"
1819

19-
"github.com/russross/blackfriday"
2020
"github.com/spf13/cobra"
2121
"github.com/spf13/pflag"
2222
"k8s.io/apimachinery/pkg/util/sets"
@@ -170,6 +170,7 @@ func (o *FeatureGateTestAnalyzerOptions) Run(ctx context.Context) error {
170170
fmt.Fprintf(o.Out, "No new Default FeatureGates found.\n")
171171
}
172172

173+
featureGateHTMLData := []utils.HTMLFeatureGate{}
173174
recentlyEnabledFeatureGates := sets.KeySet(recentlyEnabledFeatureGatesToClusterProfiles)
174175
for _, enabledFeatureGate := range sets.List(recentlyEnabledFeatureGates) {
175176
clusterProfiles := recentlyEnabledFeatureGatesToClusterProfiles[enabledFeatureGate]
@@ -197,6 +198,7 @@ func (o *FeatureGateTestAnalyzerOptions) Run(ctx context.Context) error {
197198
fmt.Fprintf(o.Out, "INSUFFICIENT CI testing for %q.\n", enabledFeatureGate)
198199
}
199200
errs = append(errs, currErrs...)
201+
featureGateHTMLData = append(featureGateHTMLData, buildHTMLFeatureGateData(enabledFeatureGate, testingResults, currErrs))
200202

201203
}
202204

@@ -207,39 +209,99 @@ func (o *FeatureGateTestAnalyzerOptions) Run(ctx context.Context) error {
207209
errs = append(errs, err)
208210
}
209211

210-
htmlContent := blackfriday.Run(summaryMarkdown)
211-
htmlBytes := []byte{}
212-
htmlBytes = append(htmlBytes, []byte(htmlHeader)...)
213-
htmlBytes = append(htmlBytes, htmlContent...)
214212
htmlFilename := filepath.Join(o.OutputDir, "feature-promotion-summary.html")
215-
if err := os.WriteFile(htmlFilename, htmlBytes, 0644); err != nil {
213+
if err := writeHTMLFromTemplate(htmlFilename, featureGateHTMLData); err != nil {
216214
errs = append(errs, err)
217215
}
218216
}
219217

220218
return errors.Join(errs...)
221219
}
222220

223-
const htmlHeader = `<head>
224-
<meta charset="UTF-8"><title>FeatureGate Promotion Summary</title>
225-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.1/css/bootstrap.min.css" integrity="sha512-T584yQ/tdRR5QwOpfvDfVQUidzfgc2339Lc8uBDtcp/wYu80d7jwBgAxbyMh0a9YM9F8N3tdErpFI8iaGx6x5g==" crossorigin="anonymous">
226-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-icons/1.5.0/font/bootstrap-icons.min.css">
227-
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
228-
<style>
229-
@media (max-width: 992px) {
230-
.container {
231-
width: 100%;
232-
max-width: none;
233-
}
234-
}
235-
table, th, td {
236-
border: 1px solid;
237-
padding: 10px;
238-
}
239-
</style>
240-
</head>
241-
242-
`
221+
func buildHTMLFeatureGateData(name string, testingResults map[JobVariant]*TestingResults, errs []error) utils.HTMLFeatureGate {
222+
jobVariantsSet := sets.KeySet(testingResults)
223+
jobVariants := jobVariantsSet.UnsortedList()
224+
sort.Sort(OrderedJobVariants(jobVariants))
225+
226+
variants := make([]utils.HTMLVariantColumn, 0, len(jobVariants))
227+
for i, jv := range jobVariants {
228+
variants = append(variants, utils.HTMLVariantColumn{
229+
Topology: jv.Topology,
230+
Cloud: jv.Cloud,
231+
Architecture: jv.Architecture,
232+
NetworkStack: jv.NetworkStack,
233+
ColIndex: i + 1,
234+
})
235+
}
236+
237+
allTests := sets.Set[string]{}
238+
for _, variantTestingResults := range testingResults {
239+
for _, currTestingResult := range variantTestingResults.TestResults {
240+
allTests.Insert(currTestingResult.TestName)
241+
}
242+
}
243+
244+
tests := make([]utils.HTMLTestRow, 0, len(allTests))
245+
for _, testName := range sets.List(allTests) {
246+
row := utils.HTMLTestRow{
247+
TestName: testName,
248+
Cells: make([]utils.HTMLTestCell, len(jobVariants)),
249+
}
250+
for i, jobVariant := range jobVariants {
251+
allTesting := testingResults[jobVariant]
252+
testResults := testResultByName(allTesting.TestResults, testName)
253+
cell := utils.HTMLTestCell{}
254+
if testResults == nil {
255+
cell.Failed = true
256+
} else {
257+
var passPercent float32
258+
if testResults.TotalRuns > 0 {
259+
passPercent = float32(testResults.SuccessfulRuns) / float32(testResults.TotalRuns)
260+
}
261+
cell.PassPercent = int(passPercent * 100)
262+
cell.SuccessfulRuns = testResults.SuccessfulRuns
263+
cell.TotalRuns = testResults.TotalRuns
264+
cell.FailedRuns = testResults.FailedRuns
265+
if testResults.TotalRuns < requiredNumberOfTestRunsPerVariant || passPercent < requiredPassRateOfTestsPerVariant {
266+
cell.Failed = true
267+
}
268+
}
269+
row.Cells[i] = cell
270+
}
271+
tests = append(tests, row)
272+
}
273+
274+
return utils.HTMLFeatureGate{
275+
Name: name,
276+
Sufficient: len(errs) == 0,
277+
Variants: variants,
278+
Tests: tests,
279+
}
280+
}
281+
282+
func writeHTMLFromTemplate(filename string, featureGateHTMLData []utils.HTMLFeatureGate) error {
283+
284+
data := utils.HTMLTemplateData{
285+
FeatureGates: featureGateHTMLData,
286+
}
287+
288+
tmpl, err := template.New("report").Parse(utils.HTMLTemplateSrc)
289+
if err != nil {
290+
return fmt.Errorf("error parsing HTML template: %w", err)
291+
}
292+
293+
f, err := os.Create(filename)
294+
if err != nil {
295+
return fmt.Errorf("error creating HTML file: %w", err)
296+
}
297+
defer f.Close()
298+
299+
if err := tmpl.Execute(f, data); err != nil {
300+
return fmt.Errorf("error executing HTML template: %w", err)
301+
}
302+
303+
return nil
304+
}
243305

244306
func checkIfTestingIsSufficient(featureGate string, testingResults map[JobVariant]*TestingResults) []error {
245307
errs := []error{}

tools/codegen/pkg/utils/html.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package utils
2+
3+
type HTMLTemplateData struct {
4+
FeatureGates []HTMLFeatureGate
5+
}
6+
7+
type HTMLFeatureGate struct {
8+
Name string
9+
Sufficient bool
10+
Variants []HTMLVariantColumn
11+
Tests []HTMLTestRow
12+
}
13+
14+
type HTMLVariantColumn struct {
15+
Topology string
16+
Cloud string
17+
Architecture string
18+
NetworkStack string
19+
ColIndex int
20+
}
21+
22+
type HTMLTestRow struct {
23+
TestName string
24+
Cells []HTMLTestCell
25+
}
26+
27+
type HTMLTestCell struct {
28+
PassPercent int
29+
SuccessfulRuns int
30+
TotalRuns int
31+
FailedRuns int
32+
Failed bool
33+
}
34+
35+
const HTMLTemplateSrc = `<!DOCTYPE html>
36+
<html>
37+
<head>
38+
<meta charset="UTF-8">
39+
<title>FeatureGate Promotion Summary</title>
40+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.1/css/bootstrap.min.css"
41+
integrity="sha512-T584yQ/tdRR5QwOpfvDfVQUidzfgc2339Lc8uBDtcp/wYu80d7jwBgAxbyMh0a9YM9F8N3tdErpFI8iaGx6x5g=="
42+
crossorigin="anonymous">
43+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
44+
<style>
45+
body { padding: 20px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
46+
.sortable { cursor: pointer; user-select: none; }
47+
.sortable:hover { background-color: #e9ecef; }
48+
.sort-indicator::after { content: ' \2195'; opacity: 0.3; font-size: 0.8em; }
49+
.sort-asc::after { content: ' \2191'; opacity: 1; font-size: 0.8em; }
50+
.sort-desc::after { content: ' \2193'; opacity: 1; font-size: 0.8em; }
51+
.fail-cell { background-color: #f8d7da; }
52+
.pass-cell { background-color: #d4edda; }
53+
.test-name { max-width: 500px; word-wrap: break-word; font-size: 0.85em; text-align: left; }
54+
table { width: 100%; border-collapse: collapse; margin-bottom: 2em; }
55+
th, td { border: 1px solid #dee2e6; padding: 8px; text-align: center; vertical-align: middle; }
56+
th:first-child, td:first-child { text-align: left; }
57+
th { background-color: #f8f9fa; }
58+
.network-stack { font-weight: bold; color: #0056b3; }
59+
.alert { padding: 12px 20px; margin-bottom: 20px; border-radius: 4px; }
60+
.alert-success { background-color: #d4edda; border: 1px solid #c3e6cb; color: #155724; }
61+
.alert-danger { background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }
62+
h1 { margin-bottom: 24px; }
63+
h2 { margin-top: 32px; margin-bottom: 12px; }
64+
</style>
65+
</head>
66+
<body>
67+
<div class="container-fluid">
68+
<h1>FeatureGate Promotion Summary</h1>
69+
{{if not .FeatureGates}}<p>No new Default FeatureGates found.</p>{{end}}
70+
{{range $fgIdx, $fg := .FeatureGates}}
71+
<h2>{{$fg.Name}}</h2>
72+
{{if $fg.Sufficient}}
73+
<div class="alert alert-success">Sufficient CI testing for &quot;{{$fg.Name}}&quot;.</div>
74+
{{else}}
75+
<div class="alert alert-danger">
76+
<strong>INSUFFICIENT</strong> CI testing for &quot;{{$fg.Name}}&quot;.
77+
<ul>
78+
<li>At least five tests are expected for a feature</li>
79+
<li>Tests must be run on every TechPreview platform (ask for an exception if your feature doesn&#39;t support a variant)</li>
80+
<li>All tests must run at least 14 times on every platform</li>
81+
<li>All tests must pass at least 95% of the time</li>
82+
</ul>
83+
</div>
84+
{{end}}
85+
{{if $fg.Tests}}
86+
<table id="table-{{$fgIdx}}">
87+
<thead>
88+
<tr>
89+
<th class="sortable sort-indicator" data-table="{{$fgIdx}}" data-col="0" data-sort="text">Test Name</th>
90+
{{range $v := $fg.Variants}}
91+
<th class="sortable sort-indicator" data-table="{{$fgIdx}}" data-col="{{$v.ColIndex}}" data-sort="percent">
92+
{{$v.Topology}}<br>{{$v.Cloud}}<br>{{$v.Architecture}}{{if $v.NetworkStack}}<br><span class="network-stack">{{$v.NetworkStack}}</span>{{end}}
93+
</th>
94+
{{end}}
95+
</tr>
96+
</thead>
97+
<tbody>
98+
{{range $test := $fg.Tests}}
99+
<tr>
100+
<td class="test-name">{{$test.TestName}}</td>
101+
{{range $cell := $test.Cells}}
102+
<td class="{{if $cell.Failed}}fail-cell{{else}}pass-cell{{end}}" data-pass-percent="{{$cell.PassPercent}}">
103+
{{if $cell.Failed}}<strong>FAIL</strong><br>{{end}}
104+
{{$cell.PassPercent}}% ({{$cell.SuccessfulRuns}} / {{$cell.TotalRuns}})
105+
{{if gt $cell.FailedRuns 0}}<br><small>{{$cell.FailedRuns}} failed</small>{{end}}
106+
</td>
107+
{{end}}
108+
</tr>
109+
{{end}}
110+
</tbody>
111+
</table>
112+
{{end}}
113+
{{end}}
114+
</div>
115+
<script>
116+
document.addEventListener('DOMContentLoaded', function() {
117+
document.querySelectorAll('th.sortable').forEach(function(th) {
118+
th.addEventListener('click', function() {
119+
sortTable(this.dataset.table, parseInt(this.dataset.col), this.dataset.sort, this);
120+
});
121+
});
122+
});
123+
124+
var sortStates = {};
125+
126+
function sortTable(tableIdx, colIdx, sortType, header) {
127+
var table = document.getElementById('table-' + tableIdx);
128+
if (!table) return;
129+
var tbody = table.querySelector('tbody');
130+
var rows = Array.from(tbody.querySelectorAll('tr'));
131+
var headers = table.querySelectorAll('th');
132+
133+
var key = tableIdx + '-' + colIdx;
134+
if (sortStates[key] === 'asc') {
135+
sortStates[key] = 'desc';
136+
} else {
137+
sortStates[key] = 'asc';
138+
}
139+
var ascending = sortStates[key] === 'asc';
140+
141+
headers.forEach(function(h) {
142+
h.classList.remove('sort-asc', 'sort-desc');
143+
h.classList.add('sort-indicator');
144+
});
145+
header.classList.remove('sort-indicator');
146+
header.classList.add(ascending ? 'sort-asc' : 'sort-desc');
147+
148+
rows.sort(function(a, b) {
149+
var valA, valB;
150+
if (sortType === 'text') {
151+
valA = a.cells[colIdx].textContent.trim().toLowerCase();
152+
valB = b.cells[colIdx].textContent.trim().toLowerCase();
153+
return ascending ? valA.localeCompare(valB) : valB.localeCompare(valA);
154+
} else {
155+
valA = parseInt(a.cells[colIdx].getAttribute('data-pass-percent') || '0', 10);
156+
valB = parseInt(b.cells[colIdx].getAttribute('data-pass-percent') || '0', 10);
157+
return ascending ? valA - valB : valB - valA;
158+
}
159+
});
160+
161+
rows.forEach(function(row) { tbody.appendChild(row); });
162+
}
163+
</script>
164+
</body>
165+
</html>
166+
`

0 commit comments

Comments
 (0)