diff --git a/tools/codegen/cmd/featuregate-test-analyzer.go b/tools/codegen/cmd/featuregate-test-analyzer.go
index 1de48e66631..a9b40cc82fc 100644
--- a/tools/codegen/cmd/featuregate-test-analyzer.go
+++ b/tools/codegen/cmd/featuregate-test-analyzer.go
@@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
+ "html/template"
"io"
"net/http"
"net/url"
@@ -16,7 +17,6 @@ import (
"strings"
"time"
- "github.com/russross/blackfriday"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"k8s.io/apimachinery/pkg/util/sets"
@@ -170,6 +170,7 @@ func (o *FeatureGateTestAnalyzerOptions) Run(ctx context.Context) error {
fmt.Fprintf(o.Out, "No new Default FeatureGates found.\n")
}
+ featureGateHTMLData := []utils.HTMLFeatureGate{}
recentlyEnabledFeatureGates := sets.KeySet(recentlyEnabledFeatureGatesToClusterProfiles)
for _, enabledFeatureGate := range sets.List(recentlyEnabledFeatureGates) {
clusterProfiles := recentlyEnabledFeatureGatesToClusterProfiles[enabledFeatureGate]
@@ -197,6 +198,7 @@ func (o *FeatureGateTestAnalyzerOptions) Run(ctx context.Context) error {
fmt.Fprintf(o.Out, "INSUFFICIENT CI testing for %q.\n", enabledFeatureGate)
}
errs = append(errs, currErrs...)
+ featureGateHTMLData = append(featureGateHTMLData, buildHTMLFeatureGateData(enabledFeatureGate, testingResults, currErrs))
}
@@ -207,12 +209,8 @@ func (o *FeatureGateTestAnalyzerOptions) Run(ctx context.Context) error {
errs = append(errs, err)
}
- htmlContent := blackfriday.Run(summaryMarkdown)
- htmlBytes := []byte{}
- htmlBytes = append(htmlBytes, []byte(htmlHeader)...)
- htmlBytes = append(htmlBytes, htmlContent...)
htmlFilename := filepath.Join(o.OutputDir, "feature-promotion-summary.html")
- if err := os.WriteFile(htmlFilename, htmlBytes, 0644); err != nil {
+ if err := writeHTMLFromTemplate(htmlFilename, featureGateHTMLData); err != nil {
errs = append(errs, err)
}
}
@@ -220,26 +218,90 @@ func (o *FeatureGateTestAnalyzerOptions) Run(ctx context.Context) error {
return errors.Join(errs...)
}
-const htmlHeader = `
- FeatureGate Promotion Summary
-
-
-
-
-
-
-`
+func buildHTMLFeatureGateData(name string, testingResults map[JobVariant]*TestingResults, errs []error) utils.HTMLFeatureGate {
+ jobVariantsSet := sets.KeySet(testingResults)
+ jobVariants := jobVariantsSet.UnsortedList()
+ sort.Sort(OrderedJobVariants(jobVariants))
+
+ variants := make([]utils.HTMLVariantColumn, 0, len(jobVariants))
+ for i, jv := range jobVariants {
+ variants = append(variants, utils.HTMLVariantColumn{
+ Topology: jv.Topology,
+ Cloud: jv.Cloud,
+ Architecture: jv.Architecture,
+ NetworkStack: jv.NetworkStack,
+ ColIndex: i + 1,
+ })
+ }
+
+ allTests := sets.Set[string]{}
+ for _, variantTestingResults := range testingResults {
+ for _, currTestingResult := range variantTestingResults.TestResults {
+ allTests.Insert(currTestingResult.TestName)
+ }
+ }
+
+ tests := make([]utils.HTMLTestRow, 0, len(allTests))
+ for _, testName := range sets.List(allTests) {
+ row := utils.HTMLTestRow{
+ TestName: testName,
+ Cells: make([]utils.HTMLTestCell, len(jobVariants)),
+ }
+ for i, jobVariant := range jobVariants {
+ allTesting := testingResults[jobVariant]
+ testResults := testResultByName(allTesting.TestResults, testName)
+ cell := utils.HTMLTestCell{}
+ if testResults == nil {
+ cell.Failed = true
+ } else {
+ var passPercent float32
+ if testResults.TotalRuns > 0 {
+ passPercent = float32(testResults.SuccessfulRuns) / float32(testResults.TotalRuns)
+ }
+ cell.PassPercent = int(passPercent * 100)
+ cell.SuccessfulRuns = testResults.SuccessfulRuns
+ cell.TotalRuns = testResults.TotalRuns
+ cell.FailedRuns = testResults.FailedRuns
+ if testResults.TotalRuns < requiredNumberOfTestRunsPerVariant || passPercent < requiredPassRateOfTestsPerVariant {
+ cell.Failed = true
+ }
+ }
+ row.Cells[i] = cell
+ }
+ tests = append(tests, row)
+ }
+
+ return utils.HTMLFeatureGate{
+ Name: name,
+ Sufficient: len(errs) == 0,
+ Variants: variants,
+ Tests: tests,
+ }
+}
+
+func writeHTMLFromTemplate(filename string, featureGateHTMLData []utils.HTMLFeatureGate) error {
+
+ data := utils.HTMLTemplateData{
+ FeatureGates: featureGateHTMLData,
+ }
+
+ tmpl, err := template.New("report").Parse(utils.HTMLTemplateSrc)
+ if err != nil {
+ return fmt.Errorf("error parsing HTML template: %w", err)
+ }
+
+ f, err := os.Create(filename)
+ if err != nil {
+ return fmt.Errorf("error creating HTML file: %w", err)
+ }
+ defer f.Close()
+
+ if err := tmpl.Execute(f, data); err != nil {
+ return fmt.Errorf("error executing HTML template: %w", err)
+ }
+
+ return nil
+}
func checkIfTestingIsSufficient(featureGate string, testingResults map[JobVariant]*TestingResults) []error {
errs := []error{}
diff --git a/tools/codegen/pkg/utils/html.go b/tools/codegen/pkg/utils/html.go
new file mode 100644
index 00000000000..faa0c8032e1
--- /dev/null
+++ b/tools/codegen/pkg/utils/html.go
@@ -0,0 +1,166 @@
+package utils
+
+type HTMLTemplateData struct {
+ FeatureGates []HTMLFeatureGate
+}
+
+type HTMLFeatureGate struct {
+ Name string
+ Sufficient bool
+ Variants []HTMLVariantColumn
+ Tests []HTMLTestRow
+}
+
+type HTMLVariantColumn struct {
+ Topology string
+ Cloud string
+ Architecture string
+ NetworkStack string
+ ColIndex int
+}
+
+type HTMLTestRow struct {
+ TestName string
+ Cells []HTMLTestCell
+}
+
+type HTMLTestCell struct {
+ PassPercent int
+ SuccessfulRuns int
+ TotalRuns int
+ FailedRuns int
+ Failed bool
+}
+
+const HTMLTemplateSrc = `
+
+
+
+ FeatureGate Promotion Summary
+
+
+
+
+
+
+
FeatureGate Promotion Summary
+ {{if not .FeatureGates}}
No new Default FeatureGates found.
{{end}}
+ {{range $fgIdx, $fg := .FeatureGates}}
+
{{$fg.Name}}
+ {{if $fg.Sufficient}}
+
Sufficient CI testing for "{{$fg.Name}}".
+ {{else}}
+
+
INSUFFICIENT CI testing for "{{$fg.Name}}".
+
+ - At least five tests are expected for a feature
+ - Tests must be run on every TechPreview platform (ask for an exception if your feature doesn't support a variant)
+ - All tests must run at least 14 times on every platform
+ - All tests must pass at least 95% of the time
+
+
+ {{end}}
+ {{if $fg.Tests}}
+
+
+
+ | Test Name |
+ {{range $v := $fg.Variants}}
+
+ {{$v.Topology}} {{$v.Cloud}} {{$v.Architecture}}{{if $v.NetworkStack}} {{$v.NetworkStack}}{{end}}
+ |
+ {{end}}
+
+
+
+ {{range $test := $fg.Tests}}
+
+ | {{$test.TestName}} |
+ {{range $cell := $test.Cells}}
+
+ {{if $cell.Failed}}FAIL {{end}}
+ {{$cell.PassPercent}}% ({{$cell.SuccessfulRuns}} / {{$cell.TotalRuns}})
+ {{if gt $cell.FailedRuns 0}} {{$cell.FailedRuns}} failed{{end}}
+ |
+ {{end}}
+
+ {{end}}
+
+
+ {{end}}
+ {{end}}
+
+
+
+
+`