diff --git a/feature/platform/transceiver/low_power_mode/tests/zrp_low_power_mode_test/README.md b/feature/platform/transceiver/low_power_mode/tests/zrp_low_power_mode_test/README.md index 012277f8223..1c258c8ad20 100644 --- a/feature/platform/transceiver/low_power_mode/tests/zrp_low_power_mode_test/README.md +++ b/feature/platform/transceiver/low_power_mode/tests/zrp_low_power_mode_test/README.md @@ -1,4 +1,4 @@ -# TRANSCEIVER-13: Configuration: 400ZR_PLUS Transceiver Low Power Mode Setting. +# TRANSCEIVER-13 (400ZR_PLUS): Configuration: 400ZR_PLUS Transceiver Low Power Mode Setting. ## Summary diff --git a/feature/platform/transceiver/low_power_mode/tests/zrp_low_power_mode_test/metadata.textproto b/feature/platform/transceiver/low_power_mode/tests/zrp_low_power_mode_test/metadata.textproto new file mode 100644 index 00000000000..6b4d3175e22 --- /dev/null +++ b/feature/platform/transceiver/low_power_mode/tests/zrp_low_power_mode_test/metadata.textproto @@ -0,0 +1,17 @@ +# proto-file: github.com/openconfig/featureprofiles/proto/metadata.proto +# proto-message: Metadata + +uuid: "8ca0676b-d1f2-4944-92c2-e65933066ca3" +plan_id: "TRANSCEIVER-13 (400ZR_PLUS)" +description: "Configuration: 400ZR_PLUS Transceiver Low Power Mode Setting." +testbed: TESTBED_DUT_400ZR_PLUS +platform_exceptions: { + platform: { + vendor: ARISTA + } + deviations: { + interface_enabled: true + default_network_instance: "default" + missing_port_to_optical_channel_component_mapping: true + } +} diff --git a/feature/platform/transceiver/low_power_mode/tests/zrp_low_power_mode_test/zrp_low_power_mode_test.go b/feature/platform/transceiver/low_power_mode/tests/zrp_low_power_mode_test/zrp_low_power_mode_test.go new file mode 100644 index 00000000000..77779450d9d --- /dev/null +++ b/feature/platform/transceiver/low_power_mode/tests/zrp_low_power_mode_test/zrp_low_power_mode_test.go @@ -0,0 +1,226 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package zrp_low_power_mode_test + +import ( + "flag" + "fmt" + "reflect" + "testing" + "time" + + "github.com/openconfig/featureprofiles/internal/cfgplugins" + "github.com/openconfig/featureprofiles/internal/components" + "github.com/openconfig/featureprofiles/internal/fptest" + "github.com/openconfig/featureprofiles/internal/samplestream" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" +) + +const ( + intUpdateTime = 5 * time.Minute + samplingInterval = 10 * time.Second + targetOutputPower = -3 + frequency = 193100000 + targetOutputPowerTolerancedBm = 1 + targetFrequencyToleranceMHz = 100000 +) + +var ( + operationalModeFlag = flag.Int("operational_mode", 5, "vendor-specific operational-mode for the channel") + operationalMode uint16 +) + +func TestMain(m *testing.M) { + fptest.RunTests(m) +} + +// validateStreamOutput validates that the OC path is streamed in the most recent subscription interval. +func validateStreamOutput(t *testing.T, streams map[string]*samplestream.SampleStream[string]) { + for key, stream := range streams { + output := stream.Next() + if output == nil { + t.Fatalf("OC path for %s not streamed in the most recent subscription interval", key) + } + value, ok := output.Val() + if !ok { + t.Fatalf("Error capturing streaming value for %s", key) + } + if reflect.TypeOf(value).Kind() != reflect.String { + t.Fatalf("Return value is not type string for key :%s", key) + } + if value == "" { + t.Fatalf("OC path empty for %s", key) + } + t.Logf("Value for OC path %s: %s", key, value) + } +} + +// validateOutputPower validates that the output power is streamed in the most recent subscription interval. +func validateOutputPower(t *testing.T, streams map[string]*samplestream.SampleStream[float64]) { + for key, stream := range streams { + outputStream := stream.Next() + if outputStream == nil { + t.Fatalf("OC path for %s not streamed in the most recent subscription interval", key) + } + outputPower, ok := outputStream.Val() + if !ok { + t.Fatalf("Error capturing streaming value for %s", key) + } + // Check output power value is of correct type + if reflect.TypeOf(outputPower).Kind() != reflect.Float64 { + t.Fatalf("Return value is not type float64 for key :%s", key) + } + t.Logf("Output power for %s: %f", key, outputPower) + } +} + +func TestLowPowerMode(t *testing.T) { + if operationalModeFlag != nil { + operationalMode = uint16(*operationalModeFlag) + } else { + t.Fatalf("Please specify the vendor-specific operational-mode flag") + } + dut := ondatra.DUT(t, "dut") + dp1 := dut.Port(t, "port1") + dp2 := dut.Port(t, "port2") + t.Logf("dut1: %v", dut) + t.Logf("dut1 dp1 name: %v", dp1.Name()) + och1 := components.OpticalChannelComponentFromPort(t, dut, dp1) + och2 := components.OpticalChannelComponentFromPort(t, dut, dp2) + cfgplugins.ConfigOpticalChannel(t, dut, och1, frequency, targetOutputPower, operationalMode) + cfgplugins.ConfigOpticalChannel(t, dut, och2, frequency, targetOutputPower, operationalMode) + for _, port := range []string{"port1", "port2"} { + t.Run(fmt.Sprintf("Port:%s", port), func(t *testing.T) { + dp := dut.Port(t, port) + gnmi.Await(t, dut, gnmi.OC().Interface(dp.Name()).OperStatus().State(), intUpdateTime, oc.Interface_OperStatus_UP) + + // Derive transceiver names from ports. + tr := gnmi.Get(t, dut, gnmi.OC().Interface(dp.Name()).Transceiver().State()) + // Stream all inventory information. + streamSerialNo := samplestream.New(t, dut, gnmi.OC().Component(tr).SerialNo().State(), samplingInterval) + defer streamSerialNo.Close() + streamPartNo := samplestream.New(t, dut, gnmi.OC().Component(tr).PartNo().State(), samplingInterval) + defer streamPartNo.Close() + streamType := samplestream.New(t, dut, gnmi.OC().Component(tr).Type().State(), samplingInterval) + defer streamType.Close() + // TODO: b/333021032 - Uncomment the description check from the test once the bug is fixed. + // streamDescription := samplestream.New(t, dut, gnmi.OC().Component(tr).Description().State(), samplingInterval) + // defer streamDescription.Close() + streamMfgName := samplestream.New(t, dut, gnmi.OC().Component(tr).MfgName().State(), samplingInterval) + defer streamMfgName.Close() + streamMfgDate := samplestream.New(t, dut, gnmi.OC().Component(tr).MfgDate().State(), samplingInterval) + defer streamMfgDate.Close() + streamHwVersion := samplestream.New(t, dut, gnmi.OC().Component(tr).HardwareVersion().State(), samplingInterval) + defer streamHwVersion.Close() + streamFirmwareVersion := samplestream.New(t, dut, gnmi.OC().Component(tr).FirmwareVersion().State(), samplingInterval) + defer streamFirmwareVersion.Close() + + allStream := map[string]*samplestream.SampleStream[string]{ + "serialNo": streamSerialNo, + "partNo": streamPartNo, + // "description": streamDescription, + "mfgName": streamMfgName, + "mfgDate": streamMfgDate, + "hwVersion": streamHwVersion, + "firmwareVersion": streamFirmwareVersion, + } + validateStreamOutput(t, allStream) + + // Disable interface + cfgplugins.ToggleInterface(t, dut, dp.Name(), false) + // Wait for interface to go down. + gnmi.Await(t, dut, gnmi.OC().Interface(dp.Name()).OperStatus().State(), intUpdateTime, oc.Interface_OperStatus_DOWN) + time.Sleep(3 * samplingInterval) // Wait an extra sample interval to ensure the device has time to process the change. + validateStreamOutput(t, allStream) + opticalChannelName := components.OpticalChannelComponentFromPort(t, dut, dp) + samplingInterval := time.Duration(gnmi.Get(t, dut, gnmi.OC().Component(opticalChannelName).OpticalChannel().OutputPower().Interval().State())) * time.Second + opInst := samplestream.New(t, dut, gnmi.OC().Component(opticalChannelName).OpticalChannel().OutputPower().Instant().State(), samplingInterval) + defer opInst.Close() + if opInstN := opInst.Next(); opInstN != nil { + if val, ok := opInstN.Val(); ok && val != -40 { + t.Logf("streaming /components/component/optical-channel/state/output-power/instant is reported: %f", val) + t.Fatalf("streaming /components/component/optical-channel/state/output-power/instant is not expected to be reported") + } + } + + opAvg := samplestream.New(t, dut, gnmi.OC().Component(opticalChannelName).OpticalChannel().OutputPower().Avg().State(), samplingInterval) + defer opAvg.Close() + if opAvgN := opAvg.Next(); opAvgN != nil { + if val, ok := opAvgN.Val(); ok && val != -40 { + t.Logf("streaming /components/component/optical-channel/state/output-power/avg is reported: %f", val) + t.Fatalf("streaming /components/component/optical-channel/state/output-power/avg is not expected to be reported") + } + } + + opMin := samplestream.New(t, dut, gnmi.OC().Component(opticalChannelName).OpticalChannel().OutputPower().Min().State(), samplingInterval) + defer opMin.Close() + if opMinN := opMin.Next(); opMinN != nil { + if val, ok := opMinN.Val(); ok && val != -40 { + t.Logf("streaming /components/component/optical-channel/state/output-power/min is reported: %f", val) + t.Fatalf("streaming /components/component/optical-channel/state/output-power/min is not expected to be reported") + } + } + + opMax := samplestream.New(t, dut, gnmi.OC().Component(opticalChannelName).OpticalChannel().OutputPower().Max().State(), samplingInterval) + defer opMax.Close() + if opMaxN := opMax.Next(); opMaxN != nil { + if val, ok := opMaxN.Val(); ok && val != -40 { + t.Logf("streaming /components/component/optical-channel/state/output-power/max is reported: %f", val) + t.Fatalf("streaming /components/component/optical-channel/state/output-power/max is not expected to be reported") + } + } + + // Enable interface + cfgplugins.ToggleInterface(t, dut, dp.Name(), true) + // Wait for interface to go up. + gnmi.Await(t, dut, gnmi.OC().Interface(dp.Name()).OperStatus().State(), intUpdateTime, oc.Interface_OperStatus_UP) + + powerStreamMap := map[string]*samplestream.SampleStream[float64]{ + "inst": opInst, + "avg": opAvg, + "min": opMin, + "max": opMax, + } + validateOutputPower(t, powerStreamMap) + // Validate the output power and frequency for the given port. + // NOTE: Default values should be removed in ValidateInterfaceConfig in cfgplugins. + // cfgplugins.ValidateInterfaceConfig(t, dut, dp) + targetOutputPowerStream := samplestream.New(t, dut, gnmi.OC().Component(opticalChannelName).OpticalChannel().TargetOutputPower().State(), samplingInterval) + defer targetOutputPowerStream.Close() + if targetOutputPowerStreamN := targetOutputPowerStream.Next(); targetOutputPowerStreamN != nil { + if val, ok := targetOutputPowerStreamN.Val(); ok { + if reflect.TypeOf(val).Kind() != reflect.Float64 { + t.Fatalf("Return value is not type float64 for key :%f", val) + } + } + } + // // TODO: jchenjian - Uncomment the output power and frequency checks from the test once the bug b/382296833 is fixed. + // outputPower := gnmi.Get(t, dut, gnmi.OC().Component(opticalChannelName).OpticalChannel().TargetOutputPower().State()) + // t.Logf("Type and value of outputPower: %T, %v", outputPower, outputPower) + // t.Logf("port: %s, Power delta: %v", dp.Name(), math.Abs(float64(outputPower)-float64(targetOutputPower))) + // if math.Abs(float64(outputPower)-float64(targetOutputPower)) > targetOutputPowerTolerancedBm { + // t.Fatalf("Output power is not within expected tolerance, got: %v want: %v tolerance: %v", outputPower, targetOutputPower, targetOutputPowerTolerancedBm) + // } + + // freq := gnmi.Get(t, dut, gnmi.OC().Component(opticalChannelName).OpticalChannel().Frequency().State()) + // t.Logf("port: %s, Frequency delta: %v", dp.Name(), math.Abs(float64(freq)-float64(frequency))) + // if math.Abs(float64(freq)-float64(frequency)) > targetFrequencyToleranceMHz { + // t.Fatalf("Frequency is not within expected tolerance, got: %v want: %v tolerance: %v", freq, frequency, targetFrequencyToleranceMHz) + // } + }) + } +} diff --git a/feature/platform/transceiver/performance_metrics/tests/zrp_pm_test/README.md b/feature/platform/transceiver/performance_metrics/tests/zrp_pm_test/README.md index 51c9e59e4e5..96ae0c6eee6 100644 --- a/feature/platform/transceiver/performance_metrics/tests/zrp_pm_test/README.md +++ b/feature/platform/transceiver/performance_metrics/tests/zrp_pm_test/README.md @@ -1,4 +1,4 @@ -# TRANSCEIVER-6: Telemetry: 400ZR_PLUS Optics performance metrics (pm) streaming. +# TRANSCEIVER-6 (400ZR_PLUS): Telemetry: 400ZR_PLUS Optics performance metrics (pm) streaming. ## Summary diff --git a/feature/platform/transceiver/performance_metrics/tests/zrp_pm_test/metadata.textproto b/feature/platform/transceiver/performance_metrics/tests/zrp_pm_test/metadata.textproto new file mode 100644 index 00000000000..e69de29bb2d diff --git a/feature/platform/transceiver/performance_metrics/tests/zrp_pm_test/zrp_pm_test.go b/feature/platform/transceiver/performance_metrics/tests/zrp_pm_test/zrp_pm_test.go new file mode 100644 index 00000000000..e2ad0a551f1 --- /dev/null +++ b/feature/platform/transceiver/performance_metrics/tests/zrp_pm_test/zrp_pm_test.go @@ -0,0 +1,223 @@ +package zrp_pm_test + +import ( + "flag" + "testing" + "time" + + "github.com/openconfig/featureprofiles/internal/cfgplugins" + "github.com/openconfig/featureprofiles/internal/components" + "github.com/openconfig/featureprofiles/internal/deviations" + "github.com/openconfig/featureprofiles/internal/fptest" + "github.com/openconfig/featureprofiles/internal/samplestream" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/openconfig/ygnmi/ygnmi" +) + +const ( + samplingInterval = 10 * time.Second + minAllowedQValue = 7.0 + maxAllowedQValue = 14.0 + minAllowedPreFECBER = 1e-9 + maxAllowedPreFECBER = 1e-2 + minAllowedESNR = 10.0 + maxAllowedESNR = 25.0 + inactiveQValue = 0.0 + inactivePreFECBER = 0.0 + inactiveESNR = 0.0 + timeout = 10 * time.Minute + otnIndexBase = uint32(4000) + ethernetIndexBase = uint32(40000) +) + +var ( + frequencies = []uint64{191400000, 196100000} + targetOpticalPowers = []float64{-7, 0} + operationalModeFlag = flag.Int("operational_mode", 5, "vendor-specific operational-mode for the channel") + operationalMode uint16 +) + +func TestMain(m *testing.M) { + fptest.RunTests(m) +} + +func TestPM(t *testing.T) { + dut := ondatra.DUT(t, "dut") + if operationalModeFlag != nil { + operationalMode = uint16(*operationalModeFlag) + } else { + t.Fatalf("Please specify the vendor-specific operational-mode flag") + } + fptest.ConfigureDefaultNetworkInstance(t, dut) + + var ( + trs = make(map[string]string) + ochs = make(map[string]string) + otnIndexes = make(map[string]uint32) + ethIndexes = make(map[string]uint32) + ) + + for i, p := range dut.Ports() { + // Check the port PMD is 400ZR_PLUS. + // Uncomment once the Ondatra OC release version is fixed. + // if p.PMD() != ondatra.PMD400GBASEZRP { + // t.Fatalf("%s PMD is %v, not 400ZR_PLUS", p.Name(), p.PMD()) + // } + + // Get transceiver and optical channel. + ochs[p.Name()] = components.OpticalChannelComponentFromPort(t, dut, p) + + // Assign OTN and ethernet indexes. + otnIndexes[p.Name()] = otnIndexBase + uint32(i) + ethIndexes[p.Name()] = ethernetIndexBase + uint32(i) + } + + for _, frequency := range frequencies { + for _, targetOpticalPower := range targetOpticalPowers { + // Configure OCH component and OTN and ETH logical channels. + for _, p := range dut.Ports() { + cfgplugins.ConfigOpticalChannel(t, dut, ochs[p.Name()], frequency, targetOpticalPower, operationalMode) + cfgplugins.ConfigOTNChannel(t, dut, ochs[p.Name()], otnIndexes[p.Name()], ethIndexes[p.Name()]) + cfgplugins.ConfigETHChannel(t, dut, p.Name(), trs[p.Name()], otnIndexes[p.Name()], ethIndexes[p.Name()]) + } + + // Create sample steams for each port. + otnStreams := make(map[string]*samplestream.SampleStream[*oc.TerminalDevice_Channel]) + interfaceStreams := make(map[string]*samplestream.SampleStream[*oc.Interface]) + for portName, otnIndex := range otnIndexes { + otnStreams[portName] = samplestream.New(t, dut, gnmi.OC().TerminalDevice().Channel(otnIndex).State(), samplingInterval) + interfaceStreams[portName] = samplestream.New(t, dut, gnmi.OC().Interface(portName).State(), samplingInterval) + defer otnStreams[portName].Close() + defer interfaceStreams[portName].Close() + } + + // Enable interface. + for _, p := range dut.Ports() { + cfgplugins.ToggleInterface(t, dut, p.Name(), true) + } + + // Wait for streaming telemetry to report the channels as up. + for _, p := range dut.Ports() { + gnmi.Await(t, dut, gnmi.OC().Interface(p.Name()).OperStatus().State(), timeout, oc.Interface_OperStatus_UP) + } + time.Sleep(3 * samplingInterval) // Wait an extra sample interval to ensure the device has time to process the change. + + validateAllSamples(t, dut, true, interfaceStreams, otnStreams) + + // Disable interface. + for _, p := range dut.Ports() { + cfgplugins.ToggleInterface(t, dut, p.Name(), false) + } + + // Wait for streaming telemetry to report the channels as down. + for _, p := range dut.Ports() { + gnmi.Await(t, dut, gnmi.OC().Interface(p.Name()).OperStatus().State(), timeout, oc.Interface_OperStatus_DOWN) + } + time.Sleep(3 * samplingInterval) // Wait an extra sample interval to ensure the device has time to process the change. + + validateAllSamples(t, dut, false, interfaceStreams, otnStreams) + + // Re-enable transceivers. + for _, p := range dut.Ports() { + cfgplugins.ToggleInterface(t, dut, p.Name(), true) + } + + // Wait for streaming telemetry to report the channels as up. + for _, p := range dut.Ports() { + gnmi.Await(t, dut, gnmi.OC().Interface(p.Name()).OperStatus().State(), timeout, oc.Interface_OperStatus_UP) + } + time.Sleep(3 * samplingInterval) // Wait an extra sample interval to ensure the device has time to process the change. + + validateAllSamples(t, dut, true, interfaceStreams, otnStreams) + } + } +} + +// validateAllSamples validates all the sample streams. +func validateAllSamples(t *testing.T, dut *ondatra.DUTDevice, isEnabled bool, interfaceStreams map[string]*samplestream.SampleStream[*oc.Interface], otnStreams map[string]*samplestream.SampleStream[*oc.TerminalDevice_Channel]) { + for _, p := range dut.Ports() { + for valIndex := range interfaceStreams[p.Name()].All() { + if valIndex >= len(otnStreams[p.Name()].All()) { + break + } + operStatus := validateSampleStream(t, dut, interfaceStreams[p.Name()].All()[valIndex], otnStreams[p.Name()].All()[valIndex], p.Name()) + switch operStatus { + case oc.Interface_OperStatus_UP: + if !isEnabled { + t.Errorf("Invalid %v operStatus value: want DOWN, got %v", p.Name(), operStatus) + } + case oc.Interface_OperStatus_DOWN: + if isEnabled { + t.Errorf("Invalid %v operStatus value: want UP, got %v", p.Name(), operStatus) + } + } + } + } +} + +// validateSampleStream validates the stream data. +func validateSampleStream(t *testing.T, dut *ondatra.DUTDevice, interfaceData *ygnmi.Value[*oc.Interface], terminalDeviceData *ygnmi.Value[*oc.TerminalDevice_Channel], portName string) oc.E_Interface_OperStatus { + if interfaceData == nil { + t.Errorf("Data not received for port %v.", portName) + return oc.Interface_OperStatus_UNSET + } + interfaceValue, ok := interfaceData.Val() + if !ok { + t.Errorf("Channel data is empty for port %v.", portName) + return oc.Interface_OperStatus_UNSET + } + operStatus := interfaceValue.GetOperStatus() + if operStatus == oc.Interface_OperStatus_UNSET { + t.Errorf("Link state data is empty for port %v", portName) + return oc.Interface_OperStatus_UNSET + } + terminalDeviceValue, ok := terminalDeviceData.Val() + if !ok { + t.Errorf("Terminal Device data is empty for port %v.", portName) + return oc.Interface_OperStatus_UNSET + } + otn := terminalDeviceValue.GetOtn() + if otn == nil { + t.Errorf("OTN data is empty for port %v", portName) + return operStatus + } + if b := otn.GetPreFecBer(); b == nil { + t.Errorf("PreFECBER data is empty for port %v", portName) + } else { + if deviations.CiscoPreFECBERInactiveValue(dut) { + validatePMValue(t, portName, "PreFECBER", b.GetInstant(), b.GetMin(), b.GetMax(), b.GetAvg(), minAllowedPreFECBER, maxAllowedPreFECBER, 0.5, operStatus) + } else { + validatePMValue(t, portName, "PreFECBER", b.GetInstant(), b.GetMin(), b.GetMax(), b.GetAvg(), minAllowedPreFECBER, maxAllowedPreFECBER, inactivePreFECBER, operStatus) + } + } + if e := otn.GetEsnr(); e == nil { + t.Errorf("ESNR data is empty for port %v", portName) + } else { + validatePMValue(t, portName, "esnr", e.GetInstant(), e.GetMin(), e.GetMax(), e.GetAvg(), minAllowedESNR, maxAllowedESNR, inactiveESNR, operStatus) + } + if q := otn.GetQValue(); q == nil { + t.Errorf("QValue data is empty for port %v", portName) + } else { + validatePMValue(t, portName, "QValue", q.GetInstant(), q.GetMin(), q.GetMax(), q.GetAvg(), minAllowedQValue, maxAllowedQValue, inactiveQValue, operStatus) + } + return operStatus +} + +// validatePMValue validates the pm value. +func validatePMValue(t *testing.T, portName, pm string, instant, min, max, avg, minAllowed, maxAllowed, inactiveValue float64, operStatus oc.E_Interface_OperStatus) { + switch operStatus { + case oc.Interface_OperStatus_UP: + if instant < minAllowed || instant > maxAllowed { + t.Errorf("Invalid %v sample when %v is UP --> min : %v, max : %v, avg : %v, instant : %v", pm, portName, min, max, avg, instant) + return + } + case oc.Interface_OperStatus_DOWN: + if instant > inactiveValue { + t.Errorf("Invalid %v sample when %v is DOWN --> min : %v, max : %v, avg : %v, instant : %v", pm, portName, min, max, avg, instant) + return + } + } + t.Logf("Valid %v sample when %v is %v --> min : %v, max : %v, avg : %v, instant : %v", pm, portName, operStatus, min, max, avg, instant) +}