-
Notifications
You must be signed in to change notification settings - Fork 796
WIP: NO-JIRA: tooling: update job-based feature promotion analysis #2944
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,11 @@ const ( | |
| // required pass rate. | ||
| // nearly all current tests pass 99% of the time, but in a two week window we lack enough data to say. | ||
| requiredPassRateOfTestsPerVariant = 0.95 | ||
|
|
||
| // required pass rate of tests per job for job-based promotion verification. | ||
| // Given a minimum sample size of 14 runs, we allow a single unique failure across job runs which equates to ~92%. | ||
| // This ensures that unique flukes don't cause a job to be considered a failed run. | ||
| requiredPassRateOfTestsPerJob = 0.92 | ||
| ) | ||
|
|
||
| type FeatureGateTestAnalyzerOptions struct { | ||
|
|
@@ -131,6 +136,7 @@ func (o *FeatureGateTestAnalyzerOptions) Run(ctx context.Context) error { | |
| md := utils.NewMarkdown("FeatureGate Promotion Summary") | ||
|
|
||
| recentlyEnabledFeatureGatesToClusterProfiles := map[string]sets.Set[string]{} | ||
| defaultCurrentlyEnabledFeatureGates := sets.Set[string]{} | ||
| errs := []error{} | ||
| for _, clusterProfile := range allCurrentClusterProfiles.List() { | ||
| // we only need to check test coverage for current cluster profiles | ||
|
|
@@ -149,6 +155,8 @@ func (o *FeatureGateTestAnalyzerOptions) Run(ctx context.Context) error { | |
| continue | ||
| } | ||
|
|
||
| defaultCurrentlyEnabledFeatureGates.Insert(featureGateName) | ||
|
|
||
| previousFeatureGateEnabled := false | ||
| if previousDefaultFeatureGateInfo != nil { | ||
| previousFeatureGateEnabled = previousDefaultFeatureGateInfo.allFeatureGates[featureGateName] | ||
|
|
@@ -181,7 +189,7 @@ func (o *FeatureGateTestAnalyzerOptions) Run(ctx context.Context) error { | |
| clusterProfiles := recentlyEnabledFeatureGatesToClusterProfiles[enabledFeatureGate] | ||
| md.Title(1, enabledFeatureGate) | ||
|
|
||
| testingResults, err := listTestResultFor(enabledFeatureGate, clusterProfiles) | ||
| testingResults, err := listTestResultFor(enabledFeatureGate, clusterProfiles, defaultCurrentlyEnabledFeatureGates) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
@@ -736,7 +744,7 @@ func validateJobTiers(jobVariant JobVariant) error { | |
| return nil | ||
| } | ||
|
|
||
| func listTestResultFor(featureGate string, clusterProfiles sets.Set[string]) (map[JobVariant]*TestingResults, error) { | ||
| func listTestResultFor(featureGate string, clusterProfiles sets.Set[string], defaultEnabledFeatureGates sets.Set[string]) (map[JobVariant]*TestingResults, error) { | ||
| fmt.Printf("Query sippy for all test run results for feature gate %q on clusterProfile %q\n", featureGate, sets.List(clusterProfiles)) | ||
|
|
||
| results := map[JobVariant]*TestingResults{} | ||
|
|
@@ -765,7 +773,7 @@ func listTestResultFor(featureGate string, clusterProfiles sets.Set[string]) (ma | |
| } | ||
|
|
||
| for _, jobVariant := range jobVariantsToCheck { | ||
| jobVariantResults, err := listTestResultForVariant(featureGate, jobVariant) | ||
| jobVariantResults, err := listTestResultForVariant(featureGate, jobVariant, defaultEnabledFeatureGates) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
@@ -862,13 +870,13 @@ func getRelease() (string, error) { | |
| return getLatestRelease() | ||
| } | ||
|
|
||
| func listTestResultForVariant(featureGate string, jobVariant JobVariant) (*TestingResults, error) { | ||
| func listTestResultForVariant(featureGate string, jobVariant JobVariant, defaultEnabledGates sets.Set[string]) (*TestingResults, error) { | ||
| // Substring here matches for both [OCPFeatureGate:...] and [FeatureGate:...] | ||
| testPattern := fmt.Sprintf("FeatureGate:%s]", featureGate) | ||
|
|
||
| // Feature gates used by the installer don't need separate tests, use the overall install tests | ||
| if strings.Contains(featureGate, "Install") { | ||
| return verifyJobBasedFeatureGatePromotion(featureGate, jobVariant) | ||
| return verifyJobBasedFeatureGatePromotion(featureGate, jobVariant, defaultEnabledGates) | ||
| } | ||
|
|
||
| fmt.Printf("Query sippy for all test run results for pattern %q on variant %#v\n", testPattern, jobVariant) | ||
|
|
@@ -990,7 +998,7 @@ func matchTwoNodeFeatureGates(featureGate string, topology string) bool { | |
| return false | ||
| } | ||
|
|
||
| func verifyJobBasedFeatureGatePromotion(featureGate string, jobVariant JobVariant) (*TestingResults, error) { | ||
| func verifyJobBasedFeatureGatePromotion(featureGate string, jobVariant JobVariant, defaultEnabledGates sets.Set[string]) (*TestingResults, error) { | ||
| ocpRelease, err := getRelease() | ||
| if err != nil { | ||
| return nil, fmt.Errorf("getting release version: %w", err) | ||
|
|
@@ -1021,7 +1029,7 @@ func verifyJobBasedFeatureGatePromotion(featureGate string, jobVariant JobVarian | |
| testResults := []TestResults{} | ||
|
|
||
| for _, job := range jobs { | ||
| results, err := verifyJobPassRate(sippyClient, ocpRelease, job, jobVariant) | ||
| results, err := verifyJobPassRate(sippyClient, ocpRelease, job, jobVariant, defaultEnabledGates) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("verifying job pass rate for job %q: %w", job.Name, err) | ||
| } | ||
|
|
@@ -1035,7 +1043,7 @@ func verifyJobBasedFeatureGatePromotion(featureGate string, jobVariant JobVarian | |
| }, nil | ||
| } | ||
|
|
||
| func verifyJobPassRate(client *http.Client, release string, job sippy.SippyJob, variant JobVariant) (*TestResults, error) { | ||
| func verifyJobPassRate(client *http.Client, release string, job sippy.SippyJob, variant JobVariant, defaultEnabledGates sets.Set[string]) (*TestResults, error) { | ||
| // Do an early check for 95% pass rate with at least 14 runs | ||
| runs := job.CurrentRuns | ||
| passes := job.CurrentPasses | ||
|
|
@@ -1053,10 +1061,10 @@ func verifyJobPassRate(client *http.Client, release string, job sippy.SippyJob, | |
| // 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, | ||
| TestName: job.Name, | ||
| TotalRuns: runs, | ||
| SuccessfulRuns: passes, | ||
| FailedRuns: runs - passes, | ||
| FailedRuns: runs - passes, | ||
| }, nil | ||
| } | ||
|
|
||
|
|
@@ -1065,15 +1073,15 @@ func verifyJobPassRate(client *http.Client, release string, job sippy.SippyJob, | |
| // | ||
| // 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 { | ||
| if float32(passes)/float32(runs) >= requiredPassRateOfTestsPerVariant { | ||
| return &TestResults{ | ||
| TestName: job.Name, | ||
| TotalRuns: runs, | ||
| TestName: job.Name, | ||
| TotalRuns: runs, | ||
| SuccessfulRuns: passes, | ||
| FailedRuns: runs - 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. | ||
|
|
@@ -1101,36 +1109,86 @@ func verifyJobPassRate(client *http.Client, release string, job sippy.SippyJob, | |
| return nil, fmt.Errorf("getting triaged test failures from sippy: %w", err) | ||
| } | ||
|
|
||
| seenFailures := map[string]int{} | ||
|
|
||
| for _, jobRun := range jobRuns { | ||
| if jobRun.OverallResult == "F" && !jobRun.KnownFailure { | ||
| // skip infrastructure related failures so they aren't included in the analysis results. | ||
| if jobRun.InfrastructureFailure { | ||
| fmt.Println("skipping job run %s due to infrastructure related failure", jobRun.TestGridURL) | ||
| continue | ||
| } | ||
|
Comment on lines
1109
to
+1119
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3. Infra failures inflate pass rate Infrastructure-failure job runs are skipped during analysis but still included in TotalRuns (len(jobRuns)) and thus treated as successes in SuccessfulRuns, inflating the computed pass rates. Agent Prompt
|
||
|
|
||
| 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 { | ||
| // filter out TPNU feature test failures | ||
| untriagedNonTechPreviewTestFailures := []string{} | ||
| for _, failure := range untriagedTestFailures { | ||
| featureGate := featureGateFromTestName(failure) | ||
| if len(featureGate) == 0 { | ||
| untriagedNonTechPreviewTestFailures = append(untriagedNonTechPreviewTestFailures, failure) | ||
| continue | ||
| } | ||
|
|
||
| // Skip this test failure if the gate is not enabled by default | ||
| if !defaultEnabledGates.Has(featureGate) { | ||
| fmt.Printf("test %q failed for job run %s but is being removed from analysis because the feature gate %q is not enabled by default\n", failure, jobRun.TestGridURL, featureGate) | ||
| continue | ||
| } | ||
|
|
||
| untriagedNonTechPreviewTestFailures = append(untriagedNonTechPreviewTestFailures, failure) | ||
| } | ||
|
|
||
| if len(untriagedNonTechPreviewTestFailures) > 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("job run %s has untriaged non-techpreview test failures:\n", jobRun.TestGridURL)) | ||
| for _, testFailure := range untriagedNonTechPreviewTestFailures { | ||
| writer.WriteString(fmt.Sprintf("\t- %s\n", testFailure)) | ||
| seenFailures[testFailure]++ | ||
| } | ||
|
|
||
| fmt.Println(writer.String()) | ||
| testResults.FailedRuns++ | ||
|
|
||
| continue | ||
| } | ||
| } | ||
| } | ||
|
|
||
| testResults.SuccessfulRuns++ | ||
| jobsFailed := 0 | ||
|
|
||
| for test, failureCount := range seenFailures { | ||
| passRate := float32(testResults.TotalRuns - failureCount)/float32(testResults.TotalRuns) | ||
| if passRate < requiredPassRateOfTestsPerJob { | ||
| fmt.Printf("test %q passed at a rate of %f%% across all job runs, which is less than the required pass rate of %f%% percent.\n", test, passRate*100, requiredPassRateOfTestsPerJob*100) | ||
|
|
||
| // Because a test can fail at most once per job run, use the highest failure count as the number of job runs that failed. | ||
| if jobsFailed < failureCount { | ||
| jobsFailed = failureCount | ||
| } | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A single sippy query such as [https://sippy-auth.dptools.openshift.org/sippy-ng/tests/5.0/details?filters=%257B%2522items%2522%253A%255B%257B%2522columnField%2522%253A%2522variants%2522%252C%2522not%2522%253Atrue%252C%2522operatorValue%2522%253A%2522has%2520entry%2522%252C%2522value%2522%253A%2522never-stable%2522%257D%252C%257B%2522columnField%2522%253A%2522variants%2522%252C%2522not%2522%253Atrue%252C%2522operatorValue%2522%253A%2522has%2520entry%2522%252C%2522value%2522%253A%2522aggregated%2522%257D%252C%257B%2522columnField%2522%253A%2522variants%2522%252C%2522operatorValue%2522%253A%2522has%2520entry%2522%252C%2522value%2522%253A%2522Capability%253AAWSDualStackInstall%2522%257D%252C%257B%2522columnField%2522%253A%2522current_working_percentage%2522%252C%2522operatorValue%2522%253A%2522%253C%2522%252C%2522value%2522%253A%252292%2522%257D%252C%257B%2522columnField%2522%253A%2522current_runs%2522%252C%2522operatorValue%2522%253A%2522%253E%253D%2522%252C%2522value%2522%253A%25220%2522%257D%255D%252C%2522linkOperator%2522%253A%2522and%2522%257D&sort=asc&sortField=current_working_percentage&view=Working|this] will get you the list of all tests below the desired 92 pass rate. The only problem is the infra filtering, but those jobs should just fail on a couple tests one of which should explicitly state infrastructure, so we want to filter that out, perhaps all "install should succeed" can go. We also want to filter out "openshift-tests should pass" (synthetic test meaning all tests must pass). Both of those can be done in the api query. The resulting list there are mostly network segmentation informing tests that are permafailing, they don't fail the job or appear in comp readiness, but sippy doesn't yet know how to filter them out. |
||
| } | ||
|
|
||
| testResults.FailedRuns = jobsFailed | ||
| testResults.SuccessfulRuns = testResults.TotalRuns - jobsFailed | ||
|
Comment on lines
+1161
to
+1176
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Count the union of failing job runs, not the largest per-test count. Taking 🤖 Prompt for AI Agents |
||
|
|
||
|
Comment on lines
+1112
to
+1177
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2. Failed runs undercounted verifyJobPassRate derives FailedRuns from the maximum per-test failure count, which can undercount failing job runs when multiple different tests exceed the per-test failure threshold on different runs, incorrectly marking the job as healthier than it is. Agent Prompt
|
||
| return testResults, nil | ||
| } | ||
|
|
||
| var featureGateRegex = regexp.MustCompile("(\\[OCPFeatureGate:.+\\])") | ||
|
|
||
| func featureGateFromTestName(failure string) string { | ||
| gateAnnotation := featureGateRegex.FindString(failure) | ||
| if len(gateAnnotation) == 0 { | ||
| return "" | ||
| } | ||
|
|
||
| return strings.TrimRight(strings.TrimLeft(gateAnnotation, "[OCPFeatureGate:"), "]") | ||
|
Comment on lines
+1181
to
+1189
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate the target file and inspect the relevant section with line numbers.
git ls-files tools/codegen/cmd/featuregate-test-analyzer.go
wc -l tools/codegen/cmd/featuregate-test-analyzer.go
sed -n '1160,1210p' tools/codegen/cmd/featuregate-test-analyzer.go
# Search for uses of featureGateRegex / featureGateFromTestName.
rg -n "featureGateRegex|featureGateFromTestName|OCPFeatureGate" tools/codegen -S
# Look for tests around this behavior.
rg -n "feature gate|featureGate|OCPFeatureGate" tools/codegen -g '*_test.go' -SRepository: openshift/api Length of output: 8310 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the tests around featureGateFromTestName and the nearby parser logic.
sed -n '1,140p' tools/codegen/cmd/featuregate-test-analyzer_test.go
sed -n '840,900p' tools/codegen/cmd/featuregate-test-analyzer.go
# Run a small read-only probe of the current regex/trim behavior on representative inputs.
python3 - <<'PY'
import re
featureGateRegex = re.compile(r"(\[OCPFeatureGate:.+\])")
samples = [
"prefix [OCPFeatureGate:Example] suffix",
"prefix [OCPFeatureGate:Example] [other] suffix",
"prefix [OCPFeatureGate:Exam]ple] suffix",
"prefix [OCPFeatureGate:[] suffix",
"prefix [FeatureGate:Example] suffix",
]
for s in samples:
m = featureGateRegex.search(s)
gate = m.group(0) if m else ""
out = gate.lstrip("[OCPFeatureGate:").rstrip("]") if gate else ""
print(f"IN : {s}")
print(f"MAT: {gate!r}")
print(f"OUT: {out!r}")
print()
PYRepository: openshift/api Length of output: 6164 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the call site and surrounding parsing logic.
sed -n '1100,1195p' tools/codegen/cmd/featuregate-test-analyzer.go
# Find where the failure string is built or sourced.
rg -n "featureGateFromTestName\(|failure\s*:|Failure|failed|OCPFeatureGate" tools/codegen/cmd/featuregate-test-analyzer.go tools/codegen/cmd/featuregate-test-analyzer_test.go -SRepository: openshift/api Length of output: 7642 Parse exactly one feature-gate annotation. The greedy 🤖 Prompt for AI Agents |
||
| } | ||
|
Comment on lines
+1181
to
+1190
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1. Featuregate parsing broken featureGateFromTestName only recognizes "[OCPFeatureGate:...]" and uses TrimLeft/TrimRight cutsets, so it can return the wrong gate name (or none) and incorrectly filter job-run failures during promotion analysis. Agent Prompt
|
||
|
|
||
| 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 { | ||
|
|
@@ -1147,7 +1205,6 @@ func getJobsForFeatureGateFromSippy(client *http.Client, release, featureGate st | |
| return nil, fmt.Errorf("reading response body: %w", err) | ||
| } | ||
|
|
||
|
|
||
| jobs := []sippy.SippyJob{} | ||
| err = json.Unmarshal(body, &jobs) | ||
| if err != nil { | ||
|
|
@@ -1158,7 +1215,7 @@ func getJobsForFeatureGateFromSippy(client *http.Client, release, featureGate st | |
| } | ||
|
|
||
| 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))) | ||
| 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) | ||
| } | ||
|
|
@@ -1173,7 +1230,6 @@ func getJobRunsFromSippy(client *http.Client, release, jobName string) ([]sippy. | |
| return nil, fmt.Errorf("reading response body: %w", err) | ||
| } | ||
|
|
||
|
|
||
| runResults := &sippy.SippyJobRunsResult{} | ||
| err = json.Unmarshal(body, runResults) | ||
| if err != nil { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
4. Misformatted infra skip log
🐞 Bug◔ ObservabilityAgent Prompt
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools