diff --git a/cmd/sippy/seed_data.go b/cmd/sippy/seed_data.go index 2d438af6d..d5a685b39 100644 --- a/cmd/sippy/seed_data.go +++ b/cmd/sippy/seed_data.go @@ -185,12 +185,22 @@ var syntheticJobs = []syntheticJobDef{ "Capability": "NetworkSegmentation", }, }, + { + nameTemplate: "periodic-ci-openshift-release-master-ci-%s-e2e-aws-ovn-amd64-capability-awsdualstackinstall", + variants: map[string]string{ + "Platform": "aws", "Architecture": "amd64", "Network": "ovn", + "Topology": "ha", "Installer": "ipi", "FeatureSet": "default", + "Suite": "parallel", "Upgrade": "none", "LayeredProduct": "none", + "Capability": "AWSDualStackInstall", + }, + }, } // Job template constants for referencing specific jobs in test specs. const awsAmd64Parallel = "periodic-ci-openshift-release-master-ci-%s-e2e-aws-ovn-amd64" const awsArm64Parallel = "periodic-ci-openshift-release-master-ci-%s-e2e-aws-ovn-arm64" const gcpAmd64Parallel = "periodic-ci-openshift-release-master-ci-%s-e2e-gcp-ovn-amd64" +const awsAmd64CapabilityAWSDualStackInstall = "periodic-ci-openshift-release-master-ci-%s-e2e-aws-ovn-amd64-capability-awsdualstackinstall" // allJobTemplates returns name templates from syntheticJobs for use in test specs // that should run on every job (e.g. install tests). @@ -363,6 +373,13 @@ var syntheticTests = []syntheticTestSpec{ awsAmd64Parallel: {"4.22": {50, 48, 0}}, }, }, + { + testID: "test-cap-aws-dual-stack-install", testName: "install should succeed: infrastructure", + component: "Installer / openshift-installer", capabilities: []string{"install"}, + jobCounts: map[string]map[string]testCount{ + awsAmd64CapabilityAWSDualStackInstall: {"4.22": {50, 47, 0}}, + }, + }, // --- Install / health indicator tests: run on every job, every release --- { testID: "test-install-overall", testName: "install should succeed: overall", diff --git a/pkg/sippyserver/server.go b/pkg/sippyserver/server.go index a25e7b2da..ff1065436 100644 --- a/pkg/sippyserver/server.go +++ b/pkg/sippyserver/server.go @@ -750,9 +750,16 @@ func injectFeatureGateHATEOASLinks(fg *apitype.FeatureGate, release, baseAPIURL, } fg.Links["tests_by_annotation"] = buildFilteredTestsURL(baseAPIURL, release, annotationFilter) + // Installer gates currently run a broad conformance suite where full passes aren't + // required, so "install should succeed" is the meaningful signal. Switch to + // "openshift-tests should work" once installer jobs run a minimal conformance suite. + capabilityTestName := "openshift-tests should work" + if strings.Contains(fg.FeatureGate, "Install") { + capabilityTestName = "install should succeed" + } capabilityFilter := filter.Filter{ Items: []filter.FilterItem{ - {Field: "name", Operator: filter.OperatorContains, Value: "openshift-tests should work"}, + {Field: "name", Operator: filter.OperatorContains, Value: capabilityTestName}, {Field: "variants", Operator: filter.OperatorContains, Value: fmt.Sprintf("Capability:%s", fg.FeatureGate)}, }, LinkOperator: filter.LinkOperatorAnd, @@ -760,7 +767,7 @@ func injectFeatureGateHATEOASLinks(fg *apitype.FeatureGate, release, baseAPIURL, fg.Links["tests_by_capability"] = buildFilteredTestsURL(baseAPIURL, release, capabilityFilter) fg.Links["ui_detail"] = fmt.Sprintf( - "%s/feature_gates/%s/%s", + "%s/sippy-ng/feature_gates/%s/%s", baseFrontendURL, release, url.PathEscape(fg.FeatureGate)) } diff --git a/sippy-ng/src/App.js b/sippy-ng/src/App.js index 6f1a4ab94..e7405ae9c 100644 --- a/sippy-ng/src/App.js +++ b/sippy-ng/src/App.js @@ -260,12 +260,23 @@ const JobsWrapper = () => { } const FeatureGateDetailWrapper = () => { - let { release, feature_gate } = useParams() + const { release, feature_gate } = useParams() const navigate = useNavigate() + const releases = React.useContext(ReleasesContext) + + const effectiveRelease = + release === 'latest' ? findFirstNonGARelease(releases) : release + + React.useEffect(() => { + if (release === 'latest' && effectiveRelease) { + navigate(`/feature_gates/${effectiveRelease}/${feature_gate}`, { + replace: true, + }) + } + }, [release, effectiveRelease, feature_gate, navigate]) if (release === 'latest') { - release = findFirstNonGARelease(React.useContext(ReleasesContext)) - navigate(`/feature_gates/${release}/${feature_gate}`, { replace: true }) + return null } return ( diff --git a/sippy-ng/src/tests/FeatureGateDetail.js b/sippy-ng/src/tests/FeatureGateDetail.js index cb2e22321..919078727 100644 --- a/sippy-ng/src/tests/FeatureGateDetail.js +++ b/sippy-ng/src/tests/FeatureGateDetail.js @@ -7,126 +7,17 @@ import { Container, Tab, Tabs, + Tooltip, Typography, } from '@mui/material' -import { DataGrid } from '@mui/x-data-grid' import { Link } from 'react-router-dom' -import { pathForTestByVariant, safeEncodeURIComponent } from '../helpers' +import { safeEncodeURIComponent } from '../helpers' import Alert from '@mui/material/Alert' +import HelpOutlineIcon from '@mui/icons-material/HelpOutline' import PropTypes from 'prop-types' import React, { Fragment, useEffect } from 'react' import SimpleBreadcrumbs from '../components/SimpleBreadcrumbs' - -function TestResultsSection({ title, apiUrl, release }) { - const [rows, setRows] = React.useState([]) - const [isLoaded, setLoaded] = React.useState(false) - const [fetchError, setFetchError] = React.useState('') - const [sortModel, setSortModel] = React.useState([ - { field: 'current_pass_percentage', sort: 'asc' }, - ]) - - useEffect(() => { - if (!apiUrl) { - setRows([]) - setLoaded(true) - return - } - setLoaded(false) - setFetchError('') - - fetch(apiUrl) - .then((response) => { - if (response.status !== 200) { - throw new Error('server returned ' + response.status) - } - return response.json() - }) - .then((json) => { - setRows(json || []) - setLoaded(true) - }) - .catch((error) => { - setFetchError('Could not retrieve ' + title + ': ' + error) - setLoaded(true) - }) - }, [apiUrl, title]) - - const columns = [ - { - field: 'name', - headerName: 'Test Name', - flex: 4, - renderCell: (params) => ( - - {params.value} - - ), - }, - { - field: 'current_successes', - headerName: 'Current Successes', - type: 'number', - flex: 1, - }, - { - field: 'current_failures', - headerName: 'Current Failures', - type: 'number', - flex: 1, - }, - { - field: 'current_flakes', - headerName: 'Current Flakes', - type: 'number', - flex: 1, - }, - { - field: 'current_pass_percentage', - headerName: 'Current Pass %', - type: 'number', - flex: 1, - renderCell: (params) => - params.value != null ? params.value.toFixed(2) + '%' : '', - }, - { - field: 'current_runs', - headerName: 'Current Runs', - type: 'number', - flex: 1, - }, - ] - - if (fetchError) { - return {fetchError} - } - - return ( - - - {title} ({isLoaded ? rows.length : '...'} tests) - - row.name} - getRowHeight={() => 'auto'} - autoHeight={true} - rowsPerPageOptions={[10, 25, 50]} - pageSize={25} - sortModel={sortModel} - onSortModelChange={setSortModel} - disableSelectionOnClick - /> - - ) -} - -TestResultsSection.propTypes = { - title: PropTypes.string.isRequired, - apiUrl: PropTypes.string, - release: PropTypes.string.isRequired, -} +import TestTable from './TestTable' export default function FeatureGateDetail(props) { const { release, featureGate } = props @@ -178,6 +69,50 @@ export default function FeatureGateDetail(props) { }) }, [release, featureGate]) + const tabAutoSelected = React.useRef(false) + + // <= 1 because the API always returns an implicit "Overall" row + const handleAnnotationDataLoaded = React.useCallback((count) => { + if (!tabAutoSelected.current && count <= 1) { + setActiveTab(1) + tabAutoSelected.current = true + } + }, []) + + const annotationFilter = { + items: [ + { + columnField: 'name', + operatorValue: 'contains', + value: `FeatureGate:${featureGate}]`, + }, + ], + } + + // Installer gates currently run a broad conformance suite where full passes + // aren't required, so "install should succeed" is the meaningful signal. + // Switch to "openshift-tests should work" once installer jobs run a minimal + // conformance suite. + const capabilityTestName = featureGate.includes('Install') + ? 'install should succeed' + : 'openshift-tests should work' + + const capabilityFilter = { + items: [ + { + columnField: 'name', + operatorValue: 'contains', + value: capabilityTestName, + }, + { + columnField: 'variants', + operatorValue: 'has entry containing', + value: `Capability:${featureGate}`, + }, + ], + linkOperator: 'and', + } + if (fetchError) { return ( @@ -231,6 +166,16 @@ export default function FeatureGateDetail(props) { Total Test Count: {gate.unique_test_count} + + + Enabled:{' '} @@ -253,21 +198,24 @@ export default function FeatureGateDetail(props) { - {activeTab === 0 && gate.links && ( - + - )} + - {activeTab === 1 && gate.links && ( - + - )} + ) diff --git a/sippy-ng/src/tests/FeatureGates.js b/sippy-ng/src/tests/FeatureGates.js index c366d2f74..f4f6370ff 100644 --- a/sippy-ng/src/tests/FeatureGates.js +++ b/sippy-ng/src/tests/FeatureGates.js @@ -296,7 +296,9 @@ export default function FeatureGates(props) { if (params.row.links && params.row.links.ui_detail) { try { const url = new URL(params.row.links.ui_detail) - return url.pathname + const basename = '/sippy-ng' + const path = url.pathname + return path.startsWith(basename) ? path.slice(basename.length) : path } catch (e) { // fall through } diff --git a/sippy-ng/src/tests/TestTable.js b/sippy-ng/src/tests/TestTable.js index efc901a10..a2f06d3f7 100644 --- a/sippy-ng/src/tests/TestTable.js +++ b/sippy-ng/src/tests/TestTable.js @@ -962,13 +962,13 @@ function TestTable(props) { return response.json() }) .then((json) => { - if (json != null) { - setRows(json) - } else { - setRows([]) - } + const data = json != null ? json : [] + setRows(data) setLoaded(true) setSearching(false) + if (props.onDataLoaded) { + props.onDataLoaded(data.length) + } }) .catch((error) => { setFetchError( @@ -991,10 +991,12 @@ function TestTable(props) { setSearching(true) fetchData() } else { - // Mark as loaded so we don't show loading spinner, and clear any stale data setRows([]) setLoaded(true) setSearching(false) + if (props.onDataLoaded) { + props.onDataLoaded(0) + } } prevLocation.current = location @@ -1178,6 +1180,7 @@ TestTable.propTypes = { sortField: PropTypes.string, rowsPerPageOptions: PropTypes.array, view: PropTypes.string, + onDataLoaded: PropTypes.func, } export default withStyles(generateClasses(TEST_THRESHOLDS))(TestTable) diff --git a/test/e2e/feature_gates_test.go b/test/e2e/feature_gates_test.go index f622bbb5f..4e442e688 100644 --- a/test/e2e/feature_gates_test.go +++ b/test/e2e/feature_gates_test.go @@ -1,6 +1,7 @@ package e2e import ( + "strings" "testing" "github.com/openshift/sippy/pkg/apis/api" @@ -54,8 +55,14 @@ func TestFeatureGatesHATEOASLinks(t *testing.T) { testsCapability, ok := fg.Links["tests_by_capability"] assert.True(t, ok, "missing tests_by_capability link") assert.Contains(t, testsCapability, "/api/tests?release="+util.Release) - assert.Contains(t, testsCapability, "openshift-tests+should+work") assert.Contains(t, testsCapability, "Capability%3A"+fg.FeatureGate) + if strings.Contains(fg.FeatureGate, "Install") { + assert.Contains(t, testsCapability, "install+should+succeed", + "Install gates should use install test for capability link") + } else { + assert.Contains(t, testsCapability, "openshift-tests+should+work", + "non-Install gates should use openshift-tests for capability link") + } uiDetail, ok := fg.Links["ui_detail"] assert.True(t, ok, "missing ui_detail link") @@ -114,6 +121,32 @@ func TestFeatureGatesCapabilityLinkFollowable(t *testing.T) { } } +func TestFeatureGatesInstallCapabilityLinkFollowable(t *testing.T) { + var gates []api.FeatureGate + err := util.SippyGet("/api/feature_gates?release="+util.Release, &gates) + require.NoError(t, err) + + gatesByName := make(map[string]api.FeatureGate) + for _, g := range gates { + gatesByName[g.FeatureGate] = g + } + + fg, ok := gatesByName["AWSDualStackInstall"] + require.True(t, ok, "AWSDualStackInstall not found") + + link := fg.Links["tests_by_capability"] + require.NotEmpty(t, link) + + var tests []api.Test + err = util.SippyGetAbsolute(link, &tests) + require.NoError(t, err, "failed to follow tests_by_capability link") + assert.Greater(t, len(tests), 0, "expected tests when following tests_by_capability link for AWSDualStackInstall") + for _, test := range tests { + assert.Contains(t, test.Name, "install should succeed", + "install gate capability tests should contain install tests") + } +} + func TestFeatureGatesFilterByName(t *testing.T) { var gates []api.FeatureGate filterJSON := `{"items":[{"columnField":"feature_gate","operatorValue":"equals","value":"NetworkSegmentation"}]}`