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"}]}`