Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
266 changes: 261 additions & 5 deletions tools/codegen/cmd/featuregate-test-analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ func (o *FeatureGateTestAnalyzerOptions) Run(ctx context.Context) error {
summaryMarkdown := md.ExactBytes()
if len(o.OutputDir) > 0 {
filename := filepath.Join(o.OutputDir, "feature-promotion-summary.md")
if err := os.WriteFile(filename, summaryMarkdown, 0644); err != nil {
if err := os.WriteFile(filename, summaryMarkdown, 0o644); err != nil {
errs = append(errs, err)
}

Expand Down Expand Up @@ -343,7 +343,6 @@ func buildHTMLFeatureGateData(name string, testingResults map[JobVariant]*Testin
}

func writeHTMLFromTemplate(filename string, featureGateHTMLData []utils.HTMLFeatureGate) error {

data := utils.HTMLTemplateData{
FeatureGates: featureGateHTMLData,
}
Expand Down Expand Up @@ -486,7 +485,6 @@ func writeTestingMarkDown(testingResults map[JobVariant]*TestingResults, md *uti
}
md.Text("")
md.Text("")

}

var (
Expand Down Expand Up @@ -649,7 +647,7 @@ func (a OrderedJobVariants) Less(i, j int) bool {

// Map these to an ordered list of strings so that we can define the order
// rather than them being alphabetical.
var networkStackOrder = map[string]string{
networkStackOrder := map[string]string{
"": "0",
"ipv4": "1",
"ipv6": "2",
Expand Down Expand Up @@ -870,7 +868,7 @@ func listTestResultForVariant(featureGate string, jobVariant JobVariant) (*Testi

// Feature gates used by the installer don't need separate tests, use the overall install tests
if strings.Contains(featureGate, "Install") {
testPattern = fmt.Sprintf("install should succeed")
return verifyJobBasedFeatureGatePromotion(featureGate, jobVariant)
}

fmt.Printf("Query sippy for all test run results for pattern %q on variant %#v\n", testPattern, jobVariant)
Expand Down Expand Up @@ -991,3 +989,261 @@ func matchTwoNodeFeatureGates(featureGate string, topology string) bool {
}
return false
}

func verifyJobBasedFeatureGatePromotion(featureGate string, jobVariant JobVariant) (*TestingResults, error) {
ocpRelease, err := getRelease()
if err != nil {
return nil, fmt.Errorf("getting release version: %w", err)
}

defaultTransport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
Comment thread
JoelSpeed marked this conversation as resolved.
}

sippyClient := &http.Client{
Timeout: 2 * time.Minute,
Transport: defaultTransport,
}

jobs, err := getJobsForFeatureGateFromSippy(sippyClient, ocpRelease, featureGate, jobVariant)
if err != nil {
return nil, fmt.Errorf("getting jobs for feature-gate %q for variant %v : %w", featureGate, jobVariant, err)
}

testResults := []TestResults{}

for _, job := range jobs {
results, err := verifyJobPassRate(sippyClient, ocpRelease, job, jobVariant)
if err != nil {
return nil, fmt.Errorf("verifying job pass rate for job %q: %w", job.Name, err)
}

testResults = append(testResults, *results)
}

return &TestingResults{
JobVariant: jobVariant,
TestResults: testResults,
}, nil
}

func verifyJobPassRate(client *http.Client, release string, job sippy.SippyJob, variant JobVariant) (*TestResults, error) {
// Do an early check for 95% pass rate with at least 14 runs
runs := job.CurrentRuns
passes := job.CurrentPasses

if runs < requiredNumberOfTestRunsPerVariant {
fmt.Printf("Insufficient results in last 7 days, increasing lookback to 2 weeks...")
runs += job.PreviousRuns
passes += job.PreviousPasses
}

// If we have less than 14 runs, return the current set of results as-is
// because it doesn't meet promotion criteria.
//
// This saves us from unnecessarily making calls out to Sippy to perform a more nuanced
// failures analysis of the job runs to see if failed runs are true failures or known regressions.
if runs < requiredNumberOfTestRunsPerVariant {
return &TestResults{
TestName: job.Name,
TotalRuns: runs,
SuccessfulRuns: passes,
FailedRuns: runs - passes,
}, nil
}

// If we have greater than or equal to 14 runs AND they are passing at a rate of at least 95%,
// we can return early because this job has passed the promotion requirements.
//
// This saves us from unnecessarily making calls out to Sippy to perform a more nuanced
// failures analysis of the job runs to see if failed runs are true failures or known regressions.
if float32(passes) / float32(runs) >= requiredPassRateOfTestsPerVariant {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return &TestResults{
TestName: job.Name,
TotalRuns: runs,
SuccessfulRuns: passes,
FailedRuns: runs - passes,
}, nil
}

// We haven't passed promotion requirements with this job, but jobs might be impacted
// by known regressed tests. While important to get fixed, many regressions are either
// release blockers or require an exception to not be a release blocker.
//
// We can be reasonably confident in promoting a feature if the tests that are failing
// on failed runs are only ones with known regressions for the platform being tested.
//
// From here on, we fetch up to the 100 most recent job runs for the job in question from Sippy,
// fetch the known regressions for the release + platform variant, and compare failing
// job runs failed tests with the known regressions - only counting failures that have
// unknown test failures as a true failure.

jobRuns, err := getJobRunsFromSippy(client, release, job.Name)
if err != nil {
return nil, fmt.Errorf("getting job %q results from sippy: %w", job.Name, err)
}

testResults := &TestResults{
TestName: job.Name,
TotalRuns: len(jobRuns),
}

triagedTestFailures, err := getTriagedTestFailuresFromSippy(client, release, variant)
if err != nil {
return nil, fmt.Errorf("getting triaged test failures from sippy: %w", err)
}

for _, jobRun := range jobRuns {
if jobRun.OverallResult == "F" && !jobRun.KnownFailure {

untriagedTestFailures := []string{}
for _, failure := range jobRun.FailedTestNames {
if !triagedTestFailures.Has(failure) {
untriagedTestFailures = append(untriagedTestFailures, failure)
}
}

if len(untriagedTestFailures) > 0 {
var writer strings.Builder
writer.WriteString(fmt.Sprintf("job run %s has untriaged test failures:\n", jobRun.TestGridURL))
for _, testFailure := range untriagedTestFailures {
writer.WriteString(fmt.Sprintf("\t- %s\n", testFailure))
}

fmt.Println(writer.String())
testResults.FailedRuns++

continue
}
}

testResults.SuccessfulRuns++
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return testResults, nil
}

func getJobsForFeatureGateFromSippy(client *http.Client, release, featureGate string, variant JobVariant) ([]sippy.SippyJob, error) {
resp, err := client.Get(sippy.BuildSippyJobsForFeatureGateURL(featureGate, release, variant.Topology, variant.Cloud, variant.Architecture, variant.NetworkStack, variant.OS))
if err != nil {
return nil, fmt.Errorf("getting job info: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("expected a 200 OK status code but got %s", resp.StatusCode)
}
Comment on lines +1141 to +1143

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Wrong format verb: %s with an int status code.

resp.StatusCode is an int, so %s renders as %!s(int=404) instead of the actual code, and go vet (run by make lint) will flag this. Use %d. Same issue at Lines 1165-1167 and 1196-1198.

🐛 Proposed fix (apply to all three sites)
-		return nil, fmt.Errorf("expected a 200 OK status code but got %s", resp.StatusCode)
+		return nil, fmt.Errorf("expected a 200 OK status code but got %d", resp.StatusCode)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("expected a 200 OK status code but got %s", resp.StatusCode)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("expected a 200 OK status code but got %d", resp.StatusCode)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/codegen/cmd/featuregate-test-analyzer.go` around lines 1139 - 1141, The
error formatting uses the wrong verb for resp.StatusCode, which is an int, so
update the fmt.Errorf messages to use %d instead of %s at this check and the
other two matching status-code checks in featuregate-test-analyzer.go. Make the
same change wherever the code compares resp.StatusCode against http.StatusOK so
the returned error string is correct and passes go vet.


body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response body: %w", err)
}


jobs := []sippy.SippyJob{}
err = json.Unmarshal(body, &jobs)
if err != nil {
return nil, fmt.Errorf("unmarshalling response body: %w", err)
}

return jobs, nil
}

func getJobRunsFromSippy(client *http.Client, release, jobName string) ([]sippy.SippyJobRun, error) {
resp, err := client.Get(sippy.BuildSippyJobRunsForJobURL(release, jobName, time.Now().Add(-1 * 14 * 24 * time.Hour)))
if err != nil {
return nil, fmt.Errorf("getting job info: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("expected a 200 OK status code but got %s", resp.StatusCode)
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response body: %w", err)
}


runResults := &sippy.SippyJobRunsResult{}
err = json.Unmarshal(body, runResults)
if err != nil {
return nil, fmt.Errorf("unmarshalling response body: %w", err)
}

return runResults.Rows, nil
}

func getTriagedTestFailuresFromSippy(client *http.Client, release string, variant JobVariant) (sets.Set[string], error) {
reqURL, err := url.Parse("https://sippy.dptools.openshift.org/api/component_readiness/triages")
if err != nil {
panic(fmt.Sprintf("couldn't parse sippy triages url: %v", err))
}
Comment on lines +1187 to +1190

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

%w is not supported by fmt.Sprintf — and prefer returning the error over panic.

fmt.Sprintf doesn't honor the %w wrapping verb, so this renders as %!w(...) and go vet (via make lint) will fail. Since the function already returns error, return the wrapped error instead of panicking.

🐛 Proposed fix
 	reqURL, err := url.Parse("https://sippy.dptools.openshift.org/api/component_readiness/triages")
 	if err != nil {
-		panic(fmt.Sprintf("couldn't parse sippy triages url: %w", err))
+		return nil, fmt.Errorf("couldn't parse sippy triages url: %w", err)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
reqURL, err := url.Parse("https://sippy.dptools.openshift.org/api/component_readiness/triages")
if err != nil {
panic(fmt.Sprintf("couldn't parse sippy triages url: %w", err))
}
reqURL, err := url.Parse("https://sippy.dptools.openshift.org/api/component_readiness/triages")
if err != nil {
return nil, fmt.Errorf("couldn't parse sippy triages url: %w", err)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/codegen/cmd/featuregate-test-analyzer.go` around lines 1187 - 1190, The
URL parse error handling in the sippy triages setup is using fmt.Sprintf with %w
and panicking, which should be replaced. In the code around the reqURL/url.Parse
logic, return the wrapped error directly from the function instead of panic, and
use proper error wrapping so the caller receives the parse failure from this
path.


resp, err := client.Get(reqURL.String())
if err != nil {
return nil, fmt.Errorf("getting sippy triages: %w", err)
}

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("expected a 200 OK status code but got %d", resp.StatusCode)
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response body: %w", err)
}

defer resp.Body.Close()

triageItems := []sippy.SippyTriageItem{}
err = json.Unmarshal(body, &triageItems)
if err != nil {
return nil, fmt.Errorf("unmarshalling response body: %w", err)
}

regressedTests := sets.New[string]()

for _, triageItem := range triageItems {
for _, regression := range triageItem.Regressions {
if regression.Release != release {
continue
}

regressionVariants := sets.New(regression.Variants...)

if !regressionVariants.Has(fmt.Sprintf("Platform:%s", variant.Cloud)) {
continue
}

if !regressionVariants.Has(fmt.Sprintf("Topology:%s", variant.Topology)) {
continue
}

if !regressionVariants.Has(fmt.Sprintf("Architecture:%s", variant.Architecture)) {
continue
}

if variant.NetworkStack != "" && !regressionVariants.Has(fmt.Sprintf("NetworkStack:%s", variant.NetworkStack)) {
continue
}

if variant.OS != "" && !regressionVariants.Has(fmt.Sprintf("OS:%s", variant.OS)) {
continue
}

regressedTests.Insert(regression.TestName)
}
}

return regressedTests, nil
}
Loading