From 808815fee3ab50658ec57f650407e40d37cd4b45 Mon Sep 17 00:00:00 2001 From: xueqzhan Date: Tue, 17 Mar 2026 16:54:12 -0400 Subject: [PATCH 1/4] Add key test filtering to test details query --- .../query/querygenerators.go | 34 +++++++++++++------ .../utils/queryparamparser.go | 4 +++ pkg/api/componentreadiness/utils/utils.go | 4 +++ 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/pkg/api/componentreadiness/query/querygenerators.go b/pkg/api/componentreadiness/query/querygenerators.go index 798f64c0bf..b3df5806a5 100644 --- a/pkg/api/componentreadiness/query/querygenerators.go +++ b/pkg/api/componentreadiness/query/querygenerators.go @@ -510,6 +510,9 @@ func BuildComponentReportQuery( // buildTestDetailsQuery returns the report for a specific test + variant combo, including job run data. // This is for the bottom level most specific pages in component readiness. +// If key test names are configured in the view's advanced options, when any of these tests fail in a job, +// all other test failures in that job are excluded from regression analysis. Only the highest priority +// (earliest in the list) key test will be included for each affected job. // TODO: I think we're querying more than we need here, there are a lot of long columns returned in this query that are // never used, test name, component, file path, url, etc. func buildTestDetailsQuery( @@ -542,14 +545,10 @@ func buildTestDetailsQuery( groupByVariants += fmt.Sprintf("jv_%s.variant_value,\n", v) } - queryString := fmt.Sprintf(` - WITH latest_component_mapping AS ( - SELECT * - FROM - %s.component_mapping cm - WHERE - created_at = (SELECT MAX(created_at) FROM %s.component_mapping) - ) + // Build WITH clause with key test filtering if configured + withClause, commonParams := buildCRQueryCTEs(client.Dataset, junitTable, c.AdvancedOption.KeyTestNames) + + queryString := fmt.Sprintf(`%s SELECT cm.id AS test_id, ANY_VALUE(test_name) AS test_name, @@ -568,7 +567,7 @@ func buildTestDetailsQuery( SUM(adjusted_flake_count) AS flake_count, FROM (%s) junit INNER JOIN latest_component_mapping cm ON testsuite = cm.suite AND test_name = cm.name -`, client.Dataset, client.Dataset, selectVariants, fmt.Sprintf(dedupedJunitTable, jobNameQueryPortion, client.Dataset, junitTable, client.Dataset, client.Dataset, jobRunAnnotationToIgnore)) +`, withClause, selectVariants, fmt.Sprintf(dedupedJunitTable, jobNameQueryPortion, client.Dataset, junitTable, client.Dataset, client.Dataset, jobRunAnnotationToIgnore)) queryString += joinVariants @@ -585,7 +584,6 @@ func buildTestDetailsQuery( (variant_registry_job_name LIKE 'periodic-%%' OR variant_registry_job_name LIKE 'release-%%' OR variant_registry_job_name LIKE 'aggregator-%%') AND ` - commonParams := []bigquery.QueryParameter{} queryString += "(" for i, testIDOption := range testIDOpts { @@ -594,6 +592,22 @@ func buildTestDetailsQuery( } queryString += ")" + // Add filtering logic for key tests with priority + // Only include the highest priority test from each job, and exclude all other tests from those jobs + if len(c.AdvancedOption.KeyTestNames) > 0 { + queryString += ` + AND ( + -- Include tests from jobs that don't have any failed key tests + junit.prowjob_build_id NOT IN (SELECT prowjob_build_id FROM jobs_with_highest_priority_test) + -- Or include only the highest priority key test from jobs that have them + OR EXISTS ( + SELECT 1 FROM jobs_with_highest_priority_test j + WHERE j.prowjob_build_id = junit.prowjob_build_id + AND j.test_name = junit.test_name + ) + )` + } + if isSample { queryString += filterByCrossCompareVariants(c.VariantOption.VariantCrossCompare, c.VariantOption.CompareVariants, &commonParams) // Only set sample release when PR and payload options are not set diff --git a/pkg/api/componentreadiness/utils/queryparamparser.go b/pkg/api/componentreadiness/utils/queryparamparser.go index f86604f8e3..3399d724d9 100644 --- a/pkg/api/componentreadiness/utils/queryparamparser.go +++ b/pkg/api/componentreadiness/utils/queryparamparser.go @@ -367,6 +367,10 @@ func parseAdvancedOptions(req *http.Request) (advancedOption reqopts.Advanced, e return advancedOption, err } + // Parse key test names - these are tests that when they fail in a job, + // all other test failures in that job are excluded from regression analysis + advancedOption.KeyTestNames = req.URL.Query()["keyTestName"] + return } diff --git a/pkg/api/componentreadiness/utils/utils.go b/pkg/api/componentreadiness/utils/utils.go index 3093e5d69a..7c076f1e86 100644 --- a/pkg/api/componentreadiness/utils/utils.go +++ b/pkg/api/componentreadiness/utils/utils.go @@ -179,6 +179,10 @@ func GenerateTestDetailsURL( params.Add("ignoreMissing", strconv.FormatBool(advancedOptions.IgnoreMissing)) params.Add("flakeAsFailure", strconv.FormatBool(advancedOptions.FlakeAsFailure)) params.Add("includeMultiReleaseAnalysis", strconv.FormatBool(advancedOptions.IncludeMultiReleaseAnalysis)) + // Add key test names if present + for _, keyTestName := range advancedOptions.KeyTestNames { + params.Add("keyTestName", keyTestName) + } if component != "" { params.Add("component", component) From a573d2c781a77c0912611f6c7d74b8d58612b70b Mon Sep 17 00:00:00 2001 From: xueqzhan Date: Thu, 19 Mar 2026 20:52:28 -0400 Subject: [PATCH 2/4] Pass view instead of key tests for test details url --- .../middleware/linkinjector/linkinjector.go | 1 + .../query/querygenerators.go | 147 ++++++++++++------ .../query/querygenerators_test.go | 18 ++- pkg/api/componentreadiness/test_details.go | 3 + pkg/api/componentreadiness/triage.go | 1 + .../utils/queryparamparser.go | 1 + pkg/api/componentreadiness/utils/utils.go | 92 +++++++---- .../componentreadiness/utils/utils_test.go | 12 ++ pkg/apis/api/componentreport/reqopts/types.go | 4 + 9 files changed, 197 insertions(+), 82 deletions(-) diff --git a/pkg/api/componentreadiness/middleware/linkinjector/linkinjector.go b/pkg/api/componentreadiness/middleware/linkinjector/linkinjector.go index 77f4d1421b..825294f583 100644 --- a/pkg/api/componentreadiness/middleware/linkinjector/linkinjector.go +++ b/pkg/api/componentreadiness/middleware/linkinjector/linkinjector.go @@ -70,6 +70,7 @@ func (l *LinkInjector) PostAnalysis(testKey crtest.Identification, testStats *te testDetailsURL, err := utils.GenerateTestDetailsURL( testKey.TestID, l.baseURL, + l.reqOptions.ViewName, // Pass the view name if present l.reqOptions.BaseRelease, l.reqOptions.SampleRelease, l.reqOptions.AdvancedOption, diff --git a/pkg/api/componentreadiness/query/querygenerators.go b/pkg/api/componentreadiness/query/querygenerators.go index b3df5806a5..cedf4895b1 100644 --- a/pkg/api/componentreadiness/query/querygenerators.go +++ b/pkg/api/componentreadiness/query/querygenerators.go @@ -290,52 +290,97 @@ func buildPriorityCaseStatement(keyTestNames []string) (string, []bigquery.Query } // buildCRQueryCTEs builds the WITH clause (Common Table Expressions) for the component readiness query. -// If keyTestNames is provided, it creates CTEs to identify jobs with failed key tests and excludes -// other test failures from those jobs. Only the highest priority (earliest in the list) key test -// will be included for each affected job. -func buildCRQueryCTEs(dataset, junitTable string, keyTestNames []string) (string, []bigquery.QueryParameter) { +// This creates the deduped_testcases CTE first, then if keyTestNames is provided, creates CTEs to identify +// jobs with failed key tests based on the DEDUPED data. This ensures key test filtering uses the same +// deduplicated results as the main query. +func buildCRQueryCTEs(dataset, junitTable, jobNameQueryPortion, jobRunAnnotationToIgnore string, keyTestNames []string) (string, []bigquery.QueryParameter) { var commonParams []bigquery.QueryParameter + // Create the deduped_testcases CTE first - this is the source of truth for all subsequent CTEs + dedupedCTE := fmt.Sprintf(`deduped_testcases AS ( + SELECT + junit.*, + ROW_NUMBER() OVER(PARTITION BY file_path, test_name, testsuite ORDER BY + CASE + WHEN flake_count > 0 THEN 0 + WHEN success_val > 0 THEN 1 + ELSE 2 + END) AS row_num, +%s + jobs.prowjob_start as prowjob_start, + jobs.prowjob_url as prowjob_url, + jobs.org, + jobs.repo, + jobs.pr_number, + jobs.pr_sha, + jobs.release_verify_tag, + CASE + WHEN flake_count > 0 THEN 0 + ELSE success_val + END AS adjusted_success_val, + CASE + WHEN flake_count > 0 THEN 1 + ELSE 0 + END AS adjusted_flake_count + FROM + %s.%s AS junit + INNER JOIN %s.jobs jobs ON + junit.prowjob_build_id = jobs.prowjob_build_id + AND jobs.prowjob_start >= DATETIME(@From) + AND jobs.prowjob_start < DATETIME(@To) + LEFT JOIN %s.job_labels job_labels ON + junit.prowjob_build_id = job_labels.prowjob_build_id + AND job_labels.prowjob_start >= DATETIME(@From) + AND job_labels.prowjob_start < DATETIME(@To) + AND job_labels.label = '%s' + WHERE modified_time >= DATETIME(@From) + AND modified_time < DATETIME(@To) + AND skipped = false + AND job_labels.label IS NULL + )`, + jobNameQueryPortion, dataset, junitTable, dataset, dataset, jobRunAnnotationToIgnore) + // Always create the component mapping CTE - componentMappingCTE := fmt.Sprintf(`latest_component_mapping AS ( - SELECT * - FROM %s.component_mapping cm - WHERE created_at = ( - SELECT MAX(created_at) - FROM %s.component_mapping))`, + componentMappingCTE := fmt.Sprintf(`, + latest_component_mapping AS ( + SELECT * + FROM %s.component_mapping cm + WHERE created_at = ( + SELECT MAX(created_at) + FROM %s.component_mapping))`, dataset, dataset) if len(keyTestNames) > 0 { - // Create a Common Table Expression (CTE) that identifies the highest priority (lowest index) key test in each job - // This ensures when multiple key tests appear in the same job, only the highest priority one is used + // Create CTEs that query from the DEDUPED data, not the raw junit table + // This ensures key test filtering uses the same deduplicated results as the main query caseStatement, caseParams := buildPriorityCaseStatement(keyTestNames) - keyTestCTEs := fmt.Sprintf(`key_test_priorities AS ( - SELECT - prowjob_build_id, - test_name, - -- Find the index/priority of each test (lower index = higher priority) - CASE - %s - END AS test_priority - FROM %s.%s AS junit - WHERE modified_time >= DATETIME(@From) - AND modified_time < DATETIME(@To) - AND test_name IN UNNEST(@KeyTestNames) - AND success_val = 0 - AND flake_count = 0 - ), - jobs_with_highest_priority_test AS ( - SELECT - prowjob_build_id, - test_name - FROM key_test_priorities - WHERE test_priority = ( - SELECT MIN(test_priority) - FROM key_test_priorities ep2 - WHERE ep2.prowjob_build_id = key_test_priorities.prowjob_build_id - ) - ),`, - caseStatement, dataset, junitTable) + keyTestCTEs := fmt.Sprintf(`, + key_test_priorities AS ( + SELECT + prowjob_build_id, + test_name, + -- Find the index/priority of each test (lower index = higher priority) + CASE + %s + END AS test_priority + FROM deduped_testcases + WHERE row_num = 1 + AND test_name IN UNNEST(@KeyTestNames) + AND adjusted_success_val = 0 + AND adjusted_flake_count = 0 + ), + jobs_with_highest_priority_test AS ( + SELECT + prowjob_build_id, + test_name + FROM key_test_priorities + WHERE test_priority = ( + SELECT MIN(test_priority) + FROM key_test_priorities ep2 + WHERE ep2.prowjob_build_id = key_test_priorities.prowjob_build_id + ) + )`, + caseStatement) commonParams = append(commonParams, caseParams...) commonParams = append(commonParams, bigquery.QueryParameter{ @@ -343,10 +388,10 @@ func buildCRQueryCTEs(dataset, junitTable string, keyTestNames []string) (string Value: keyTestNames, }) - return fmt.Sprintf("WITH %s\n%s", keyTestCTEs, componentMappingCTE), commonParams + return fmt.Sprintf("WITH %s%s%s", dedupedCTE, keyTestCTEs, componentMappingCTE), commonParams } - return fmt.Sprintf("WITH %s", componentMappingCTE), commonParams + return fmt.Sprintf("WITH %s%s", dedupedCTE, componentMappingCTE), commonParams } // BuildComponentReportQuery returns the common query for the higher level summary component summary. @@ -387,7 +432,7 @@ func BuildComponentReportQuery( // TODO: last_failure here explicitly uses success_val not adjusted_success_val, this ensures we // show the last time the test failed, not flaked. if you enable the flakes as failures feature (which is // non default today), the last failure time will be wrong which can impact things like failed fix detection. - withClause, commonParams := buildCRQueryCTEs(client.Dataset, junitTable, reqOptions.AdvancedOption.KeyTestNames) + withClause, commonParams := buildCRQueryCTEs(client.Dataset, junitTable, jobNameQueryPortion, jobRunAnnotationToIgnore, reqOptions.AdvancedOption.KeyTestNames) queryString := fmt.Sprintf(`%s SELECT @@ -401,15 +446,16 @@ func BuildComponentReportQuery( MAX(CASE WHEN junit_data.success_val = 0 THEN junit_data.prowjob_start ELSE NULL END) AS last_failure, ANY_VALUE(cm.component) AS component, ANY_VALUE(cm.capabilities) AS capabilities, - FROM (%s) AS junit_data + FROM deduped_testcases AS junit_data INNER JOIN latest_component_mapping cm ON junit_data.testsuite = cm.suite AND junit_data.test_name = cm.name `, - withClause, selectVariants, fmt.Sprintf(dedupedJunitTable, jobNameQueryPortion, client.Dataset, junitTable, client.Dataset, client.Dataset, jobRunAnnotationToIgnore)) + withClause, selectVariants) queryString += joinVariants - queryString += `WHERE cm.staff_approved_obsolete = false AND - (junit_data.variant_registry_job_name LIKE 'periodic-%%' OR junit_data.variant_registry_job_name LIKE 'release-%%' OR junit_data.variant_registry_job_name LIKE 'aggregator-%%')` + queryString += `WHERE junit_data.row_num = 1 + AND cm.staff_approved_obsolete = false + AND (junit_data.variant_registry_job_name LIKE 'periodic-%%' OR junit_data.variant_registry_job_name LIKE 'release-%%' OR junit_data.variant_registry_job_name LIKE 'aggregator-%%')` // Add filtering logic for key tests with priority // Only include the highest priority test from each job, and exclude all other tests from those jobs @@ -546,7 +592,7 @@ func buildTestDetailsQuery( } // Build WITH clause with key test filtering if configured - withClause, commonParams := buildCRQueryCTEs(client.Dataset, junitTable, c.AdvancedOption.KeyTestNames) + withClause, commonParams := buildCRQueryCTEs(client.Dataset, junitTable, jobNameQueryPortion, jobRunAnnotationToIgnore, c.AdvancedOption.KeyTestNames) queryString := fmt.Sprintf(`%s SELECT @@ -565,9 +611,9 @@ func buildTestDetailsQuery( ANY_VALUE(cm.capabilities) as capabilities, SUM(adjusted_success_val) AS success_count, SUM(adjusted_flake_count) AS flake_count, - FROM (%s) junit + FROM deduped_testcases junit INNER JOIN latest_component_mapping cm ON testsuite = cm.suite AND test_name = cm.name -`, withClause, selectVariants, fmt.Sprintf(dedupedJunitTable, jobNameQueryPortion, client.Dataset, junitTable, client.Dataset, client.Dataset, jobRunAnnotationToIgnore)) +`, withClause, selectVariants) queryString += joinVariants @@ -581,7 +627,8 @@ func buildTestDetailsQuery( modified_time `, groupByVariants) queryString += ` WHERE - (variant_registry_job_name LIKE 'periodic-%%' OR variant_registry_job_name LIKE 'release-%%' OR variant_registry_job_name LIKE 'aggregator-%%') + junit.row_num = 1 + AND (variant_registry_job_name LIKE 'periodic-%%' OR variant_registry_job_name LIKE 'release-%%' OR variant_registry_job_name LIKE 'aggregator-%%') AND ` diff --git a/pkg/api/componentreadiness/query/querygenerators_test.go b/pkg/api/componentreadiness/query/querygenerators_test.go index 4c63719c29..09cfd4366b 100644 --- a/pkg/api/componentreadiness/query/querygenerators_test.go +++ b/pkg/api/componentreadiness/query/querygenerators_test.go @@ -102,12 +102,14 @@ func TestBuildComponentReportQuery_ExclusiveTestFiltering(t *testing.T) { "Query should contain jobs_with_highest_priority_test CTE") assert.Contains(t, commonQuery, "key_test_priorities", "Query should contain exclusive_test_priorities CTE for priority calculation") - assert.Contains(t, commonQuery, "AND success_val = 0", - "CTE should only identify jobs where key tests FAILED (success_val = 0)") + assert.Contains(t, commonQuery, "AND adjusted_success_val = 0", + "CTE should only identify jobs where key tests FAILED (adjusted_success_val = 0 from deduped data)") assert.Contains(t, commonQuery, "test_name IN UNNEST(@KeyTestNames)", "CTE should filter by key test names") assert.Contains(t, commonQuery, "test_priority", "CTE should calculate test priority based on list order") + assert.Contains(t, commonQuery, "FROM deduped_testcases", + "CTE should query from deduped_testcases, not raw junit table") } else { assert.NotContains(t, commonQuery, "jobs_with_highest_priority_test", "Query should not contain jobs_with_highest_priority_test CTE when no key tests") @@ -201,18 +203,22 @@ func TestBuildComponentReportQuery_ExclusiveTestLogic(t *testing.T) { // The query should: // 1. Create CTEs that identify the highest priority test in each job - assert.Contains(t, commonQuery, "WITH key_test_priorities AS", + assert.Contains(t, commonQuery, "WITH deduped_testcases AS", + "Should create deduped_testcases CTE first") + assert.Contains(t, commonQuery, "key_test_priorities AS", "Should create CTE for calculating test priorities") assert.Contains(t, commonQuery, "jobs_with_highest_priority_test AS", "Should create CTE for identifying highest priority test per job") - // 2. The CTE should check success_val = 0 (failure) + // 2. The CTE should check adjusted_success_val = 0 (failure) from deduped data cteEnd := strings.Index(commonQuery, "latest_component_mapping") require.Greater(t, cteEnd, 0, "Should contain latest_component_mapping CTE") cteSection := commonQuery[:cteEnd] - assert.Contains(t, cteSection, "success_val = 0", - "CTE should only match FAILED key tests (success_val = 0), not all instances") + assert.Contains(t, cteSection, "adjusted_success_val = 0", + "CTE should only match FAILED key tests (adjusted_success_val = 0 from deduped data), not all instances") + assert.Contains(t, cteSection, "FROM deduped_testcases", + "CTE should query from deduped_testcases, not raw junit table") // 3. Priority-based filtering: only include highest priority test from jobs with key test failures assert.Contains(t, commonQuery, "test_priority", diff --git a/pkg/api/componentreadiness/test_details.go b/pkg/api/componentreadiness/test_details.go index 0899a88b15..80907b3b9d 100644 --- a/pkg/api/componentreadiness/test_details.go +++ b/pkg/api/componentreadiness/test_details.go @@ -308,9 +308,12 @@ func (c *ComponentReportGenerator) GenerateDetailsReportForTest( } // Generate test details URL with the newer sample date range + // For the "latest" link, we should not use the view (as the view's date range may be stale) + // Instead, we generate a full URL with updated sample dates latestURL, err := utils.GenerateTestDetailsURL( testIDOption.TestID, c.baseURL, + "", // Don't use view for "latest" link - we want fresh dates, not view's dates c.ReqOptions.BaseRelease, newSampleRelease, c.ReqOptions.AdvancedOption, diff --git a/pkg/api/componentreadiness/triage.go b/pkg/api/componentreadiness/triage.go index f2251efbc1..9b565f0036 100644 --- a/pkg/api/componentreadiness/triage.go +++ b/pkg/api/componentreadiness/triage.go @@ -779,6 +779,7 @@ func generateTestDetailsURLFromRegression(regression *models.TestRegression, vie return utils.GenerateTestDetailsURL( regression.TestID, baseURL, + view.Name, // Pass the view name baseReleaseOpts, sampleReleaseOpts, view.AdvancedOptions, diff --git a/pkg/api/componentreadiness/utils/queryparamparser.go b/pkg/api/componentreadiness/utils/queryparamparser.go index 3399d724d9..24ea1d030d 100644 --- a/pkg/api/componentreadiness/utils/queryparamparser.go +++ b/pkg/api/componentreadiness/utils/queryparamparser.go @@ -38,6 +38,7 @@ func ParseComponentReportRequest( if view != nil { // set params from view + opts.ViewName = view.Name opts.VariantOption = view.VariantOptions opts.AdvancedOption = view.AdvancedOptions opts.TestFilters = view.TestFilters diff --git a/pkg/api/componentreadiness/utils/utils.go b/pkg/api/componentreadiness/utils/utils.go index 7c076f1e86..1df079eb62 100644 --- a/pkg/api/componentreadiness/utils/utils.go +++ b/pkg/api/componentreadiness/utils/utils.go @@ -92,12 +92,47 @@ func VariantsMapToStringSlice(variants map[string]string) []string { return vs } -// GenerateTestDetailsURL creates a HATEOAS-style URL for the test_details API endpoint -// based on explicit parameters. This function is focused on URL generation rather than -// data processing, with the caller responsible for extracting the required data. +// addVariantParams adds variant parameters to url.Values and returns the environment string. +// This helper consolidates the duplicate variant parameter logic used in both view-based +// and legacy URL generation. +func addVariantParams(params url.Values, variantMap map[string]string) { + if len(variantMap) == 0 { + return + } + + // Sort the keys to ensure consistent parameter ordering + variantKeys := make([]string, 0, len(variantMap)) + for key := range variantMap { + variantKeys = append(variantKeys, key) + } + sort.Strings(variantKeys) + + // Add individual variant parameters and build environment string + environment := make([]string, 0, len(variantMap)) + for _, key := range variantKeys { + value := variantMap[key] + params.Add(key, value) + environment = append(environment, fmt.Sprintf("%s:%s", key, value)) + } + + // Add environment parameter (space-separated variant pairs) + params.Add("environment", strings.Join(environment, " ")) +} + +// GenerateTestDetailsURL creates a HATEOAS-style URL for the test_details API endpoint. +// +// When viewName is provided, generates a minimal URL with just the view parameter plus test-specific overrides: +// - view= +// - testId= +// - component, capability (if provided) +// - specific variants from the variants parameter +// - testBasisRelease (if baseReleaseOverride is provided) +// +// When viewName is empty, generates a full URL with all parameters for backward compatibility. func GenerateTestDetailsURL( testID string, baseURL string, + viewName string, baseReleaseOpts reqopts.Release, sampleReleaseOpts reqopts.Release, advancedOptions reqopts.Advanced, @@ -140,6 +175,32 @@ func GenerateTestDetailsURL( params := url.Values{} + // If a view is provided, generate a minimal URL with just the view + test-specific overrides + if viewName != "" { + params.Add("view", viewName) + params.Add("testId", testID) + + // Add test-specific parameters (component, capability, specific variants) + if component != "" { + params.Add("component", component) + } + if capability != "" { + params.Add("capability", capability) + } + + // Add variant parameters and environment string + addVariantParams(params, variantMap) + + // Check if release fallback was used and add the override + if baseReleaseOverride != "" && baseReleaseOverride != baseReleaseOpts.Name { + params.Add("testBasisRelease", baseReleaseOverride) + } + + u.RawQuery = params.Encode() + return u.String(), nil + } + + // Otherwise, generate full URL with all parameters (for backward compatibility) params.Add("testId", testID) // Add release and time parameters @@ -179,10 +240,6 @@ func GenerateTestDetailsURL( params.Add("ignoreMissing", strconv.FormatBool(advancedOptions.IgnoreMissing)) params.Add("flakeAsFailure", strconv.FormatBool(advancedOptions.FlakeAsFailure)) params.Add("includeMultiReleaseAnalysis", strconv.FormatBool(advancedOptions.IncludeMultiReleaseAnalysis)) - // Add key test names if present - for _, keyTestName := range advancedOptions.KeyTestNames { - params.Add("keyTestName", keyTestName) - } if component != "" { params.Add("component", component) @@ -261,25 +318,8 @@ func GenerateTestDetailsURL( } } - // Add the specific variants as individual parameters - // Sort the keys to ensure consistent environment parameter ordering - variantKeys := make([]string, 0, len(variantMap)) - for key := range variantMap { - variantKeys = append(variantKeys, key) - } - sort.Strings(variantKeys) - - environment := make([]string, 0, len(variantMap)) - for _, key := range variantKeys { - value := variantMap[key] - params.Add(key, value) - environment = append(environment, fmt.Sprintf("%s:%s", key, value)) - } - - // Add environment parameter (space-separated variant pairs) - if len(environment) > 0 { - params.Add("environment", strings.Join(environment, " ")) - } + // Add variant parameters and environment string + addVariantParams(params, variantMap) u.RawQuery = params.Encode() return u.String(), nil diff --git a/pkg/api/componentreadiness/utils/utils_test.go b/pkg/api/componentreadiness/utils/utils_test.go index ed990ae881..4cda99dcff 100644 --- a/pkg/api/componentreadiness/utils/utils_test.go +++ b/pkg/api/componentreadiness/utils/utils_test.go @@ -69,6 +69,7 @@ func TestGenerateTestDetailsURL(t *testing.T) { url, err := GenerateTestDetailsURL( "test-id", "", + "", // viewName getBaseReleaseOpts(), getSampleReleaseOpts(), testView.AdvancedOptions, @@ -87,6 +88,7 @@ func TestGenerateTestDetailsURL(t *testing.T) { _, err := GenerateTestDetailsURL( "", "https://example.com", + "", // viewName getBaseReleaseOpts(), getSampleReleaseOpts(), testView.AdvancedOptions, @@ -105,6 +107,7 @@ func TestGenerateTestDetailsURL(t *testing.T) { url, err := GenerateTestDetailsURL( "test-id", "", + "", // viewName getBaseReleaseOpts(), getSampleReleaseOpts(), testView.AdvancedOptions, @@ -127,6 +130,7 @@ func TestGenerateTestDetailsURL(t *testing.T) { url, err := GenerateTestDetailsURL( "test-id", "", + "", // viewName getBaseReleaseOpts(), getSampleReleaseOpts(), testView.AdvancedOptions, @@ -148,6 +152,7 @@ func TestGenerateTestDetailsURL(t *testing.T) { url, err := GenerateTestDetailsURL( "openshift-tests:abc123", "https://sippy.example.com", + "", // viewName getBaseReleaseOpts(), getSampleReleaseOpts(), testView.AdvancedOptions, @@ -181,6 +186,7 @@ func TestGenerateTestDetailsURL(t *testing.T) { url, err := GenerateTestDetailsURL( "openshift-tests:abc123", "https://sippy.example.com", + "", // viewName getBaseReleaseOpts(), getSampleReleaseOpts(), testView.AdvancedOptions, @@ -247,6 +253,7 @@ func TestGenerateTestDetailsURL(t *testing.T) { url, err := GenerateTestDetailsURL( "openshift-tests:9f3fb60052539c29ab66564689f616ce", "https://sippy-auth.dptools.openshift.org", + "", // viewName baseReleaseOpts, sampleReleaseOpts, realWorldView.AdvancedOptions, @@ -344,6 +351,7 @@ func TestGenerateTestDetailsURL(t *testing.T) { url, err := GenerateTestDetailsURL( "test-id", "https://example.com", + "", // viewName baseReleaseOpts, sampleReleaseOpts, viewWithVariants.AdvancedOptions, @@ -416,6 +424,7 @@ func TestGenerateTestDetailsURL(t *testing.T) { url, err := GenerateTestDetailsURL( "test-id-123", "https://example.com", + "", // viewName baseReleaseOpts, sampleReleaseOpts, viewWithCrossCompare.AdvancedOptions, @@ -463,6 +472,7 @@ func TestGenerateTestDetailsURL(t *testing.T) { url, err := GenerateTestDetailsURL( "test-id", "https://sippy.example.com", + "", // viewName getBaseReleaseOpts(), sampleReleaseWithPR, testView.AdvancedOptions, @@ -496,6 +506,7 @@ func TestGenerateTestDetailsURL(t *testing.T) { url, err := GenerateTestDetailsURL( "test-id", "https://sippy.example.com", + "", // viewName getBaseReleaseOpts(), sampleReleaseWithPayload, testView.AdvancedOptions, @@ -524,6 +535,7 @@ func TestGenerateTestDetailsURL(t *testing.T) { url, err := GenerateTestDetailsURL( "test-id", "https://sippy.example.com", + "", // viewName getBaseReleaseOpts(), getSampleReleaseOpts(), testView.AdvancedOptions, diff --git a/pkg/apis/api/componentreport/reqopts/types.go b/pkg/apis/api/componentreport/reqopts/types.go index 90247f6c06..bba5202d74 100644 --- a/pkg/apis/api/componentreport/reqopts/types.go +++ b/pkg/apis/api/componentreport/reqopts/types.go @@ -19,6 +19,10 @@ type RequestOptions struct { CacheOption cache.RequestOptions TestFilters TestIDOptions []TestIdentification + // ViewName is the name of the view used for this request, if any. + // When generating test details URLs, if a view is present, we include just the view parameter + // plus test-specific overrides, rather than expanding all view parameters into the URL. + ViewName string `json:"view_name,omitempty" yaml:"view_name,omitempty"` } // PullRequest specifies a specific pull request to use as the From b340414868160ffdf216451787c3996fa588585f Mon Sep 17 00:00:00 2001 From: xueqzhan Date: Fri, 20 Mar 2026 13:06:04 -0400 Subject: [PATCH 3/4] Solve lint failures --- .../query/querygenerators.go | 104 +++----- pkg/api/componentreadiness/utils/utils.go | 227 ++++++++++-------- 2 files changed, 162 insertions(+), 169 deletions(-) diff --git a/pkg/api/componentreadiness/query/querygenerators.go b/pkg/api/componentreadiness/query/querygenerators.go index cedf4895b1..3305759394 100644 --- a/pkg/api/componentreadiness/query/querygenerators.go +++ b/pkg/api/componentreadiness/query/querygenerators.go @@ -28,69 +28,6 @@ const ( DefaultJunitTable = "junit" jobRunAnnotationToIgnore = "InfraFailure" - // This query de-dupes the test results. There are multiple issues present in - // our data set: - // - // 1. Some test suites in OpenShift retry, resulting in potentially multiple - // failures for the same test in a job. Component Readiness is currently - // counting these as separate failures, resulting in an outsized impact on - // our statistical analysis. - // - // 2. There is a second bug where successful test cases are sometimes - // recorded by openshift-tests more than once, it's tracked by - // https://issues.redhat.com/browse/OCPBUGS-16039 - // - // 3. Flaked tests also have rows for the failures, so we need to ensure we only collect the flakes. - // - // 4. Flaked tests appear to be able to have success_val as 0 or 1. - // - // So, this sorts the data, partitioning by the 3-tuple of file_path/test_name/testsuite - - // preferring flakes, then successes, then failures, and we get the first row of each - // partition. - dedupedJunitTable = ` - WITH deduped_testcases AS ( - SELECT - junit.*, - ROW_NUMBER() OVER(PARTITION BY file_path, test_name, testsuite ORDER BY - CASE - WHEN flake_count > 0 THEN 0 - WHEN success_val > 0 THEN 1 - ELSE 2 - END) AS row_num, -%s - jobs.prowjob_start as prowjob_start, - jobs.prowjob_url as prowjob_url, - jobs.org, - jobs.repo, - jobs.pr_number, - jobs.pr_sha, - jobs.release_verify_tag, - CASE - WHEN flake_count > 0 THEN 0 - ELSE success_val - END AS adjusted_success_val, - CASE - WHEN flake_count > 0 THEN 1 - ELSE 0 - END AS adjusted_flake_count - FROM - %s.%s AS junit - INNER JOIN %s.jobs jobs ON - junit.prowjob_build_id = jobs.prowjob_build_id - AND jobs.prowjob_start >= DATETIME(@From) - AND jobs.prowjob_start < DATETIME(@To) - LEFT JOIN %s.job_labels job_labels ON - junit.prowjob_build_id = job_labels.prowjob_build_id - AND job_labels.prowjob_start >= DATETIME(@From) - AND job_labels.prowjob_start < DATETIME(@To) - AND job_labels.label = '%s' - WHERE modified_time >= DATETIME(@From) - AND modified_time < DATETIME(@To) - AND skipped = false - AND job_labels.label IS NULL - ) - SELECT * FROM deduped_testcases WHERE row_num = 1` - // normalJobNameCol simply uses the prow job name for regular (non-pull-request) component reports. normalJobNameCol = ` jobs.prowjob_job_name AS variant_registry_job_name, @@ -290,9 +227,22 @@ func buildPriorityCaseStatement(keyTestNames []string) (string, []bigquery.Query } // buildCRQueryCTEs builds the WITH clause (Common Table Expressions) for the component readiness query. -// This creates the deduped_testcases CTE first, then if keyTestNames is provided, creates CTEs to identify -// jobs with failed key tests based on the DEDUPED data. This ensures key test filtering uses the same -// deduplicated results as the main query. +// +// The deduped_testcases CTE handles multiple issues in our dataset: +// 1. Test suite retries can result in multiple failures for the same test in a job +// 2. Successful tests are sometimes recorded multiple times (OCPBUGS-16039) +// 3. Flaked tests have rows for both failures and flakes +// 4. Flaked tests can have success_val as 0 or 1 +// +// The deduplication logic partitions by file_path/test_name/testsuite and prefers: +// - Flakes first (flake_count > 0) +// - Then successes (success_val > 0) +// - Then failures +// +// We take only the first row (row_num = 1) from each partition. +// +// If keyTestNames is provided, additional CTEs are created to identify jobs with failed key tests +// based on the DEDUPED data. This ensures key test filtering uses the same deduplicated results. func buildCRQueryCTEs(dataset, junitTable, jobNameQueryPortion, jobRunAnnotationToIgnore string, keyTestNames []string) (string, []bigquery.QueryParameter) { var commonParams []bigquery.QueryParameter @@ -458,18 +408,23 @@ func BuildComponentReportQuery( AND (junit_data.variant_registry_job_name LIKE 'periodic-%%' OR junit_data.variant_registry_job_name LIKE 'release-%%' OR junit_data.variant_registry_job_name LIKE 'aggregator-%%')` // Add filtering logic for key tests with priority - // Only include the highest priority test from each job, and exclude all other tests from those jobs + // Exclude other failing tests from jobs with failed key tests, but keep passing/flaky tests if len(reqOptions.AdvancedOption.KeyTestNames) > 0 { queryString += ` AND ( -- Include tests from jobs that don't have any failed key tests junit_data.prowjob_build_id NOT IN (SELECT prowjob_build_id FROM jobs_with_highest_priority_test) - -- Or include only the highest priority key test from jobs that have them + -- Or include the highest priority key test from jobs that have failed key tests OR EXISTS ( SELECT 1 FROM jobs_with_highest_priority_test j WHERE j.prowjob_build_id = junit_data.prowjob_build_id AND j.test_name = junit_data.test_name ) + -- Or include non-failing tests (successful or flaky) from jobs with failed key tests + OR ( + junit_data.prowjob_build_id IN (SELECT prowjob_build_id FROM jobs_with_highest_priority_test) + AND (junit_data.adjusted_success_val > 0 OR junit_data.adjusted_flake_count > 0) + ) )` } if reqOptions.AdvancedOption.IgnoreDisruption { @@ -640,18 +595,23 @@ func buildTestDetailsQuery( queryString += ")" // Add filtering logic for key tests with priority - // Only include the highest priority test from each job, and exclude all other tests from those jobs + // Exclude other failing tests from jobs with failed key tests, but keep passing/flaky tests if len(c.AdvancedOption.KeyTestNames) > 0 { queryString += ` AND ( -- Include tests from jobs that don't have any failed key tests junit.prowjob_build_id NOT IN (SELECT prowjob_build_id FROM jobs_with_highest_priority_test) - -- Or include only the highest priority key test from jobs that have them + -- Or include the highest priority key test from jobs that have failed key tests OR EXISTS ( SELECT 1 FROM jobs_with_highest_priority_test j WHERE j.prowjob_build_id = junit.prowjob_build_id AND j.test_name = junit.test_name ) + -- Or include non-failing tests (successful or flaky) from jobs with failed key tests + OR ( + junit.prowjob_build_id IN (SELECT prowjob_build_id FROM jobs_with_highest_priority_test) + AND (junit.adjusted_success_val > 0 OR junit.adjusted_flake_count > 0) + ) )` } @@ -975,10 +935,10 @@ func (s *sampleTestDetailsQueryGenerator) QueryTestStatus(ctx context.Context) ( sampleString := commonQuery if s.ReqOptions.SampleRelease.PullRequestOptions != nil { - sampleString += ` AND jobs.org = @Org AND jobs.repo = @Repo AND jobs.pr_number = @PRNumber` + sampleString += ` AND junit.org = @Org AND junit.repo = @Repo AND junit.pr_number = @PRNumber` } if s.ReqOptions.SampleRelease.PayloadOptions != nil { - sampleString += ` AND jobs.release_verify_tag IN UNNEST(@Tags)` + sampleString += ` AND junit.release_verify_tag IN UNNEST(@Tags)` } sampleQuery := s.client.Query(ctx, bqlabel.TDJunitSample, sampleString+groupByQuery) sampleQuery.Parameters = append(sampleQuery.Parameters, queryParameters...) diff --git a/pkg/api/componentreadiness/utils/utils.go b/pkg/api/componentreadiness/utils/utils.go index 1df079eb62..e5654583e9 100644 --- a/pkg/api/componentreadiness/utils/utils.go +++ b/pkg/api/componentreadiness/utils/utils.go @@ -92,6 +92,18 @@ func VariantsMapToStringSlice(variants map[string]string) []string { return vs } +// parseVariantsToMap converts a slice of "key:value" strings to a map +func parseVariantsToMap(variants []string) map[string]string { + variantMap := make(map[string]string) + for _, variant := range variants { + parts := strings.SplitN(variant, ":", 2) + if len(parts) == 2 { + variantMap[parts[0]] = parts[1] + } + } + return variantMap +} + // addVariantParams adds variant parameters to url.Values and returns the environment string. // This helper consolidates the duplicate variant parameter logic used in both view-based // and legacy URL generation. @@ -119,91 +131,43 @@ func addVariantParams(params url.Values, variantMap map[string]string) { params.Add("environment", strings.Join(environment, " ")) } -// GenerateTestDetailsURL creates a HATEOAS-style URL for the test_details API endpoint. -// -// When viewName is provided, generates a minimal URL with just the view parameter plus test-specific overrides: -// - view= -// - testId= -// - component, capability (if provided) -// - specific variants from the variants parameter -// - testBasisRelease (if baseReleaseOverride is provided) -// -// When viewName is empty, generates a full URL with all parameters for backward compatibility. -func GenerateTestDetailsURL( - testID string, - baseURL string, +// buildViewBasedURL generates a minimal URL with just view + test-specific overrides +func buildViewBasedURL( + params url.Values, viewName string, - baseReleaseOpts reqopts.Release, - sampleReleaseOpts reqopts.Release, - advancedOptions reqopts.Advanced, - variantOptions reqopts.Variants, - testFilters reqopts.TestFilters, + testID string, component string, capability string, - variants []string, + variantMap map[string]string, + baseReleaseOpts reqopts.Release, baseReleaseOverride string, -) (string, error) { - - if testID == "" { - return "", fmt.Errorf("testID cannot be empty") - } - - // Parse variants from the variants slice (which is a []string of "key:value" pairs) - variantMap := make(map[string]string) - for _, variant := range variants { - parts := strings.SplitN(variant, ":", 2) - if len(parts) == 2 { - variantMap[parts[0]] = parts[1] - } - } +) { + params.Add("view", viewName) + params.Add("testId", testID) - // Build the URL with query parameters - var fullURL string - if baseURL == "" { - // For backward compatibility, return relative URL if no baseURL provided - logrus.Warn("GenerateTestDetailsURL was given an empty baseURL") - fullURL = "/api/component_readiness/test_details" - } else { - // Create fully qualified URL - fullURL = fmt.Sprintf("%s/api/component_readiness/test_details", baseURL) + if component != "" { + params.Add("component", component) } - - u, err := url.Parse(fullURL) - if err != nil { - return "", fmt.Errorf("failed to parse URL: %w", err) + if capability != "" { + params.Add("capability", capability) } - params := url.Values{} - - // If a view is provided, generate a minimal URL with just the view + test-specific overrides - if viewName != "" { - params.Add("view", viewName) - params.Add("testId", testID) - - // Add test-specific parameters (component, capability, specific variants) - if component != "" { - params.Add("component", component) - } - if capability != "" { - params.Add("capability", capability) - } - - // Add variant parameters and environment string - addVariantParams(params, variantMap) - - // Check if release fallback was used and add the override - if baseReleaseOverride != "" && baseReleaseOverride != baseReleaseOpts.Name { - params.Add("testBasisRelease", baseReleaseOverride) - } + // Add variant parameters and environment string + addVariantParams(params, variantMap) - u.RawQuery = params.Encode() - return u.String(), nil + // Check if release fallback was used and add the override + if baseReleaseOverride != "" && baseReleaseOverride != baseReleaseOpts.Name { + params.Add("testBasisRelease", baseReleaseOverride) } +} - // Otherwise, generate full URL with all parameters (for backward compatibility) - params.Add("testId", testID) - - // Add release and time parameters +// addReleaseParams adds release-related parameters (dates, PR, payload options) +func addReleaseParams( + params url.Values, + baseReleaseOpts reqopts.Release, + sampleReleaseOpts reqopts.Release, + baseReleaseOverride string, +) { params.Add("baseRelease", baseReleaseOpts.Name) params.Add("sampleRelease", sampleReleaseOpts.Name) params.Add("baseStartTime", baseReleaseOpts.Start.Format("2006-01-02T15:04:05Z")) @@ -229,8 +193,10 @@ func GenerateTestDetailsURL( if baseReleaseOverride != "" && baseReleaseOverride != baseReleaseOpts.Name { params.Add("testBasisRelease", baseReleaseOverride) } +} - // Add advanced options +// addAdvancedOptionsParams adds advanced options to URL parameters +func addAdvancedOptionsParams(params url.Values, advancedOptions reqopts.Advanced) { params.Add("confidence", strconv.Itoa(advancedOptions.Confidence)) params.Add("minFail", strconv.Itoa(advancedOptions.MinimumFailure)) params.Add("pity", strconv.Itoa(advancedOptions.PityFactor)) @@ -240,23 +206,10 @@ func GenerateTestDetailsURL( params.Add("ignoreMissing", strconv.FormatBool(advancedOptions.IgnoreMissing)) params.Add("flakeAsFailure", strconv.FormatBool(advancedOptions.FlakeAsFailure)) params.Add("includeMultiReleaseAnalysis", strconv.FormatBool(advancedOptions.IncludeMultiReleaseAnalysis)) +} - if component != "" { - params.Add("component", component) - } - if capability != "" { - params.Add("capability", capability) - } - - // Add test filter parameters - for _, cap := range testFilters.Capabilities { - params.Add("testCapabilities", cap) - } - for _, lifecycle := range testFilters.Lifecycles { - params.Add("testLifecycles", lifecycle) - } - - // Add variant options +// addVariantOptionsParams adds variant options to URL parameters +func addVariantOptionsParams(params url.Values, variantOptions reqopts.Variants) { if variantOptions.ColumnGroupBy != nil { params.Add("columnGroupBy", strings.Join(variantOptions.ColumnGroupBy.List(), ",")) } @@ -265,7 +218,6 @@ func GenerateTestDetailsURL( } // Add include variants - // Sort variant keys to ensure consistent parameter ordering includeVariantKeys := make([]string, 0, len(variantOptions.IncludeVariants)) for variantKey := range variantOptions.IncludeVariants { includeVariantKeys = append(includeVariantKeys, variantKey) @@ -274,7 +226,6 @@ func GenerateTestDetailsURL( for _, variantKey := range includeVariantKeys { variantValues := variantOptions.IncludeVariants[variantKey] - // Sort variant values to ensure consistent parameter ordering sortedValues := make([]string, len(variantValues)) copy(sortedValues, variantValues) sort.Strings(sortedValues) @@ -284,9 +235,8 @@ func GenerateTestDetailsURL( } } - // Add compare variants (for cross-compare feature) + // Add compare variants if len(variantOptions.CompareVariants) > 0 { - // Sort variant keys to ensure consistent parameter ordering compareVariantKeys := make([]string, 0, len(variantOptions.CompareVariants)) for variantKey := range variantOptions.CompareVariants { compareVariantKeys = append(compareVariantKeys, variantKey) @@ -295,7 +245,6 @@ func GenerateTestDetailsURL( for _, variantKey := range compareVariantKeys { variantValues := variantOptions.CompareVariants[variantKey] - // Sort variant values to ensure consistent parameter ordering sortedValues := make([]string, len(variantValues)) copy(sortedValues, variantValues) sort.Strings(sortedValues) @@ -308,7 +257,6 @@ func GenerateTestDetailsURL( // Add variant cross compare if len(variantOptions.VariantCrossCompare) > 0 { - // Sort to ensure consistent parameter ordering sortedCrossCompare := make([]string, len(variantOptions.VariantCrossCompare)) copy(sortedCrossCompare, variantOptions.VariantCrossCompare) sort.Strings(sortedCrossCompare) @@ -317,6 +265,91 @@ func GenerateTestDetailsURL( params.Add("variantCrossCompare", variantKey) } } +} + +// GenerateTestDetailsURL creates a HATEOAS-style URL for the test_details API endpoint. +// +// When viewName is provided, generates a minimal URL with just the view parameter plus test-specific overrides: +// - view= +// - testId= +// - component, capability (if provided) +// - specific variants from the variants parameter +// - testBasisRelease (if baseReleaseOverride is provided) +// +// When viewName is empty, generates a full URL with all parameters for backward compatibility. +func GenerateTestDetailsURL( + testID string, + baseURL string, + viewName string, + baseReleaseOpts reqopts.Release, + sampleReleaseOpts reqopts.Release, + advancedOptions reqopts.Advanced, + variantOptions reqopts.Variants, + testFilters reqopts.TestFilters, + component string, + capability string, + variants []string, + baseReleaseOverride string, +) (string, error) { + + if testID == "" { + return "", fmt.Errorf("testID cannot be empty") + } + + // Parse variants from the variants slice + variantMap := parseVariantsToMap(variants) + + // Build the URL with query parameters + var fullURL string + if baseURL == "" { + // For backward compatibility, return relative URL if no baseURL provided + logrus.Warn("GenerateTestDetailsURL was given an empty baseURL") + fullURL = "/api/component_readiness/test_details" + } else { + // Create fully qualified URL + fullURL = fmt.Sprintf("%s/api/component_readiness/test_details", baseURL) + } + + u, err := url.Parse(fullURL) + if err != nil { + return "", fmt.Errorf("failed to parse URL: %w", err) + } + + params := url.Values{} + + // If a view is provided, generate a minimal URL with just the view + test-specific overrides + if viewName != "" { + buildViewBasedURL(params, viewName, testID, component, capability, variantMap, baseReleaseOpts, baseReleaseOverride) + u.RawQuery = params.Encode() + return u.String(), nil + } + + // Otherwise, generate full URL with all parameters (for backward compatibility) + params.Add("testId", testID) + + // Add release parameters + addReleaseParams(params, baseReleaseOpts, sampleReleaseOpts, baseReleaseOverride) + + // Add advanced options + addAdvancedOptionsParams(params, advancedOptions) + + if component != "" { + params.Add("component", component) + } + if capability != "" { + params.Add("capability", capability) + } + + // Add test filter parameters + for _, cap := range testFilters.Capabilities { + params.Add("testCapabilities", cap) + } + for _, lifecycle := range testFilters.Lifecycles { + params.Add("testLifecycles", lifecycle) + } + + // Add variant options + addVariantOptionsParams(params, variantOptions) // Add variant parameters and environment string addVariantParams(params, variantMap) From 56b756bec7eb6827fc1af31298bb01095b6fdee2 Mon Sep 17 00:00:00 2001 From: xueqzhan Date: Fri, 20 Mar 2026 14:48:36 -0400 Subject: [PATCH 4/4] remove some duplicate codes --- .../query/querygenerators.go | 78 ++++++++----------- .../query/querygenerators_test.go | 6 +- pkg/api/componentreadiness/utils/utils.go | 1 + 3 files changed, 37 insertions(+), 48 deletions(-) diff --git a/pkg/api/componentreadiness/query/querygenerators.go b/pkg/api/componentreadiness/query/querygenerators.go index 3305759394..1b4fb556d9 100644 --- a/pkg/api/componentreadiness/query/querygenerators.go +++ b/pkg/api/componentreadiness/query/querygenerators.go @@ -226,6 +226,28 @@ func buildPriorityCaseStatement(keyTestNames []string) (string, []bigquery.Query return strings.Join(caseStatements, "\n\t\t\t\t\t\t\t"), params } +// buildKeyTestFilterClause returns the WHERE clause for filtering by key tests with priority. +// It excludes other failing tests from jobs with failed key tests, but keeps passing/flaky tests. +// The tableAlias parameter specifies the alias for the deduped_testcases table (e.g., "junit_data" or "junit"). +func buildKeyTestFilterClause(tableAlias string) string { + return fmt.Sprintf(` + AND ( + -- Include tests from jobs that don't have any failed key tests + %s.prowjob_build_id NOT IN (SELECT prowjob_build_id FROM jobs_with_highest_priority_test) + -- Or include the highest priority key test from jobs that have failed key tests + OR EXISTS ( + SELECT 1 FROM jobs_with_highest_priority_test j + WHERE j.prowjob_build_id = %s.prowjob_build_id + AND j.test_name = %s.test_name + ) + -- Or include non-failing tests (successful or flaky) from jobs with failed key tests + OR ( + %s.prowjob_build_id IN (SELECT prowjob_build_id FROM jobs_with_highest_priority_test) + AND (%s.adjusted_success_val > 0 OR %s.adjusted_flake_count > 0) + ) + )`, tableAlias, tableAlias, tableAlias, tableAlias, tableAlias, tableAlias) +} + // buildCRQueryCTEs builds the WITH clause (Common Table Expressions) for the component readiness query. // // The deduped_testcases CTE handles multiple issues in our dataset: @@ -239,15 +261,13 @@ func buildPriorityCaseStatement(keyTestNames []string) (string, []bigquery.Query // - Then successes (success_val > 0) // - Then failures // -// We take only the first row (row_num = 1) from each partition. -// // If keyTestNames is provided, additional CTEs are created to identify jobs with failed key tests // based on the DEDUPED data. This ensures key test filtering uses the same deduplicated results. func buildCRQueryCTEs(dataset, junitTable, jobNameQueryPortion, jobRunAnnotationToIgnore string, keyTestNames []string) (string, []bigquery.QueryParameter) { var commonParams []bigquery.QueryParameter - // Create the deduped_testcases CTE first - this is the source of truth for all subsequent CTEs - dedupedCTE := fmt.Sprintf(`deduped_testcases AS ( + // Create the deduped_testcases CTE - this is the source of truth for all subsequent CTEs + dedupedCTE := fmt.Sprintf(`deduped_testcases_with_rownum AS ( SELECT junit.*, ROW_NUMBER() OVER(PARTITION BY file_path, test_name, testsuite ORDER BY @@ -287,6 +307,9 @@ func buildCRQueryCTEs(dataset, junitTable, jobNameQueryPortion, jobRunAnnotation AND modified_time < DATETIME(@To) AND skipped = false AND job_labels.label IS NULL + ), + deduped_testcases AS ( + SELECT * FROM deduped_testcases_with_rownum WHERE row_num = 1 )`, jobNameQueryPortion, dataset, junitTable, dataset, dataset, jobRunAnnotationToIgnore) @@ -314,8 +337,7 @@ func buildCRQueryCTEs(dataset, junitTable, jobNameQueryPortion, jobRunAnnotation %s END AS test_priority FROM deduped_testcases - WHERE row_num = 1 - AND test_name IN UNNEST(@KeyTestNames) + WHERE test_name IN UNNEST(@KeyTestNames) AND adjusted_success_val = 0 AND adjusted_flake_count = 0 ), @@ -403,29 +425,11 @@ func BuildComponentReportQuery( queryString += joinVariants - queryString += `WHERE junit_data.row_num = 1 - AND cm.staff_approved_obsolete = false + queryString += `WHERE cm.staff_approved_obsolete = false AND (junit_data.variant_registry_job_name LIKE 'periodic-%%' OR junit_data.variant_registry_job_name LIKE 'release-%%' OR junit_data.variant_registry_job_name LIKE 'aggregator-%%')` - // Add filtering logic for key tests with priority - // Exclude other failing tests from jobs with failed key tests, but keep passing/flaky tests if len(reqOptions.AdvancedOption.KeyTestNames) > 0 { - queryString += ` - AND ( - -- Include tests from jobs that don't have any failed key tests - junit_data.prowjob_build_id NOT IN (SELECT prowjob_build_id FROM jobs_with_highest_priority_test) - -- Or include the highest priority key test from jobs that have failed key tests - OR EXISTS ( - SELECT 1 FROM jobs_with_highest_priority_test j - WHERE j.prowjob_build_id = junit_data.prowjob_build_id - AND j.test_name = junit_data.test_name - ) - -- Or include non-failing tests (successful or flaky) from jobs with failed key tests - OR ( - junit_data.prowjob_build_id IN (SELECT prowjob_build_id FROM jobs_with_highest_priority_test) - AND (junit_data.adjusted_success_val > 0 OR junit_data.adjusted_flake_count > 0) - ) - )` + queryString += buildKeyTestFilterClause("junit_data") } if reqOptions.AdvancedOption.IgnoreDisruption { queryString += ` AND NOT 'Disruption' in UNNEST(capabilities)` @@ -582,8 +586,7 @@ func buildTestDetailsQuery( modified_time `, groupByVariants) queryString += ` WHERE - junit.row_num = 1 - AND (variant_registry_job_name LIKE 'periodic-%%' OR variant_registry_job_name LIKE 'release-%%' OR variant_registry_job_name LIKE 'aggregator-%%') + (variant_registry_job_name LIKE 'periodic-%%' OR variant_registry_job_name LIKE 'release-%%' OR variant_registry_job_name LIKE 'aggregator-%%') AND ` @@ -594,25 +597,8 @@ func buildTestDetailsQuery( } queryString += ")" - // Add filtering logic for key tests with priority - // Exclude other failing tests from jobs with failed key tests, but keep passing/flaky tests if len(c.AdvancedOption.KeyTestNames) > 0 { - queryString += ` - AND ( - -- Include tests from jobs that don't have any failed key tests - junit.prowjob_build_id NOT IN (SELECT prowjob_build_id FROM jobs_with_highest_priority_test) - -- Or include the highest priority key test from jobs that have failed key tests - OR EXISTS ( - SELECT 1 FROM jobs_with_highest_priority_test j - WHERE j.prowjob_build_id = junit.prowjob_build_id - AND j.test_name = junit.test_name - ) - -- Or include non-failing tests (successful or flaky) from jobs with failed key tests - OR ( - junit.prowjob_build_id IN (SELECT prowjob_build_id FROM jobs_with_highest_priority_test) - AND (junit.adjusted_success_val > 0 OR junit.adjusted_flake_count > 0) - ) - )` + queryString += buildKeyTestFilterClause("junit") } if isSample { diff --git a/pkg/api/componentreadiness/query/querygenerators_test.go b/pkg/api/componentreadiness/query/querygenerators_test.go index 09cfd4366b..d98834dd38 100644 --- a/pkg/api/componentreadiness/query/querygenerators_test.go +++ b/pkg/api/componentreadiness/query/querygenerators_test.go @@ -203,8 +203,10 @@ func TestBuildComponentReportQuery_ExclusiveTestLogic(t *testing.T) { // The query should: // 1. Create CTEs that identify the highest priority test in each job - assert.Contains(t, commonQuery, "WITH deduped_testcases AS", - "Should create deduped_testcases CTE first") + assert.Contains(t, commonQuery, "WITH deduped_testcases_with_rownum AS", + "Should create deduped_testcases_with_rownum CTE first") + assert.Contains(t, commonQuery, "deduped_testcases AS", + "Should create deduped_testcases CTE that filters to row_num = 1") assert.Contains(t, commonQuery, "key_test_priorities AS", "Should create CTE for calculating test priorities") assert.Contains(t, commonQuery, "jobs_with_highest_priority_test AS", diff --git a/pkg/api/componentreadiness/utils/utils.go b/pkg/api/componentreadiness/utils/utils.go index e5654583e9..7d9fc6a588 100644 --- a/pkg/api/componentreadiness/utils/utils.go +++ b/pkg/api/componentreadiness/utils/utils.go @@ -277,6 +277,7 @@ func addVariantOptionsParams(params url.Values, variantOptions reqopts.Variants) // - testBasisRelease (if baseReleaseOverride is provided) // // When viewName is empty, generates a full URL with all parameters for backward compatibility. +// Note: keyTestNames are NOT included in URLs - they come from the view definition on the server. func GenerateTestDetailsURL( testID string, baseURL string,