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}}". + +
+ {{end}} + {{if $fg.Tests}} + + + + + {{range $v := $fg.Variants}} + + {{end}} + + + + {{range $test := $fg.Tests}} + + + {{range $cell := $test.Cells}} + + {{end}} + + {{end}} + +
Test Name + {{$v.Topology}}
{{$v.Cloud}}
{{$v.Architecture}}{{if $v.NetworkStack}}
{{$v.NetworkStack}}{{end}} +
{{$test.TestName}} + {{if $cell.Failed}}FAIL
{{end}} + {{$cell.PassPercent}}% ({{$cell.SuccessfulRuns}} / {{$cell.TotalRuns}}) + {{if gt $cell.FailedRuns 0}}
{{$cell.FailedRuns}} failed{{end}} +
+ {{end}} + {{end}} +
+ + + +`