pull/3588/merge
tdakkota 2 weeks ago committed by GitHub
commit f2bf72bfd9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 73
      collector/ethtool_linux.go
  2. 26
      collector/ethtool_linux_test.go
  3. 215
      collector/ethtool_sfp_linux.go
  4. 426
      collector/ethtool_sfp_linux_test.go
  5. BIN
      collector/fixtures/ethtool/eth0/module_eeprom

@ -26,6 +26,7 @@ import (
"os"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"syscall"
@ -49,6 +50,7 @@ type Ethtool interface {
DriverInfo(string) (ethtool.DrvInfo, error)
Stats(string) (map[string]uint64, error)
LinkInfo(string) (ethtool.EthtoolCmd, error)
ModuleEeprom(string) ([]byte, error)
}
type ethtoolLibrary struct {
@ -69,15 +71,24 @@ func (e *ethtoolLibrary) LinkInfo(intf string) (ethtool.EthtoolCmd, error) {
return ethtoolCmd, err
}
func (e *ethtoolLibrary) ModuleEeprom(intf string) ([]byte, error) {
return e.ethtool.ModuleEeprom(intf)
}
type ethtoolCollector struct {
fs sysfs.FS
entries map[string]*prometheus.Desc
entriesMutex sync.Mutex
ethtool Ethtool
deviceFilter deviceFilter
infoDesc *prometheus.Desc
metricsPattern *regexp.Regexp
logger *slog.Logger
fs sysfs.FS
entries map[string]*prometheus.Desc
entriesMutex sync.Mutex
ethtool Ethtool
deviceFilter deviceFilter
infoDesc *prometheus.Desc
moduleTemperatureDesc *prometheus.Desc
moduleVoltageDesc *prometheus.Desc
moduleTxBiasDesc *prometheus.Desc
moduleTxPowerDesc *prometheus.Desc
moduleRxPowerDesc *prometheus.Desc
metricsPattern *regexp.Regexp
logger *slog.Logger
}
// makeEthtoolCollector is the internal constructor for EthtoolCollector.
@ -111,6 +122,31 @@ func makeEthtoolCollector(logger *slog.Logger) (*ethtoolCollector, error) {
deviceFilter: newDeviceFilter(*ethtoolDeviceExclude, *ethtoolDeviceInclude),
metricsPattern: regexp.MustCompile(*ethtoolIncludedMetrics),
logger: logger,
moduleTemperatureDesc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "ethtool", "module_temperature_celsius"),
"Module temperature in degrees Celsius",
[]string{"device"}, nil,
),
moduleVoltageDesc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "ethtool", "module_voltage_volts"),
"Module supply voltage in volts",
[]string{"device"}, nil,
),
moduleTxBiasDesc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "ethtool", "module_tx_bias_milliamperes"),
"Module TX laser bias current in milliamperes",
[]string{"device", "lane"}, nil,
),
moduleTxPowerDesc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "ethtool", "module_tx_power_milliwatts"),
"Module TX optical power in milliwatts",
[]string{"device", "lane"}, nil,
),
moduleRxPowerDesc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "ethtool", "module_rx_power_milliwatts"),
"Module RX optical power in milliwatts",
[]string{"device", "lane"}, nil,
),
entries: map[string]*prometheus.Desc{
"rx_bytes": prometheus.NewDesc(
prometheus.BuildFQName(namespace, "ethtool", "received_bytes_total"),
@ -445,6 +481,27 @@ func (c *ethtoolCollector) Update(ch chan<- prometheus.Metric) error {
}
}
eepromData, err := c.ethtool.ModuleEeprom(device)
if err == nil {
modMetrics, parseErr := parseModuleEeprom(eepromData)
if parseErr == nil {
ch <- prometheus.MustNewConstMetric(c.moduleTemperatureDesc, prometheus.GaugeValue, modMetrics.temperature, device)
ch <- prometheus.MustNewConstMetric(c.moduleVoltageDesc, prometheus.GaugeValue, modMetrics.voltage, device)
for i, lane := range modMetrics.lanes {
laneStr := strconv.Itoa(i + 1)
ch <- prometheus.MustNewConstMetric(c.moduleTxBiasDesc, prometheus.GaugeValue, lane.txBias, device, laneStr)
ch <- prometheus.MustNewConstMetric(c.moduleTxPowerDesc, prometheus.GaugeValue, lane.txPower, device, laneStr)
ch <- prometheus.MustNewConstMetric(c.moduleRxPowerDesc, prometheus.GaugeValue, lane.rxPower, device, laneStr)
}
} else {
c.logger.Debug("ethtool module EEPROM parse error", "err", parseErr, "device", device)
}
} else if err != unix.EOPNOTSUPP {
c.logger.Error("ethtool module EEPROM error", "err", err, "device", device)
} else {
c.logger.Debug("ethtool module EEPROM error", "err", err, "device", device)
}
if len(stats) == 0 {
// No stats returned; device does not support ethtool stats.
continue

@ -17,6 +17,7 @@ package collector
import (
"bufio"
"errors"
"fmt"
"io"
"log/slog"
@ -257,6 +258,14 @@ func (e *EthtoolFixture) LinkInfo(intf string) (ethtool.EthtoolCmd, error) {
return res, err
}
func (e *EthtoolFixture) ModuleEeprom(intf string) ([]byte, error) {
data, err := os.ReadFile(filepath.Join(e.fixturePath, intf, "module_eeprom"))
if errors.Is(err, os.ErrNotExist) {
return nil, unix.EOPNOTSUPP
}
return data, err
}
func NewEthtoolTestCollector(logger *slog.Logger) (Collector, error) {
collector, err := makeEthtoolCollector(logger)
if err != nil {
@ -288,7 +297,22 @@ func TestBuildEthtoolFQName(t *testing.T) {
}
func TestEthToolCollector(t *testing.T) {
testcase := `# HELP node_ethtool_align_errors Network interface align_errors
testcase := `# HELP node_ethtool_module_rx_power_milliwatts Module RX optical power in milliwatts
# TYPE node_ethtool_module_rx_power_milliwatts gauge
node_ethtool_module_rx_power_milliwatts{device="eth0",lane="1"} 0.5
# HELP node_ethtool_module_temperature_celsius Module temperature in degrees Celsius
# TYPE node_ethtool_module_temperature_celsius gauge
node_ethtool_module_temperature_celsius{device="eth0"} 25
# HELP node_ethtool_module_tx_bias_milliamperes Module TX laser bias current in milliamperes
# TYPE node_ethtool_module_tx_bias_milliamperes gauge
node_ethtool_module_tx_bias_milliamperes{device="eth0",lane="1"} 20
# HELP node_ethtool_module_tx_power_milliwatts Module TX optical power in milliwatts
# TYPE node_ethtool_module_tx_power_milliwatts gauge
node_ethtool_module_tx_power_milliwatts{device="eth0",lane="1"} 1
# HELP node_ethtool_module_voltage_volts Module supply voltage in volts
# TYPE node_ethtool_module_voltage_volts gauge
node_ethtool_module_voltage_volts{device="eth0"} 3.2976
# HELP node_ethtool_align_errors Network interface align_errors
# TYPE node_ethtool_align_errors untyped
node_ethtool_align_errors{device="eth0"} 0
# HELP node_ethtool_info A metric with a constant '1' value labeled by bus_info, device, driver, expansion_rom_version, firmware_version, version.

@ -0,0 +1,215 @@
// Copyright 2021 The Prometheus Authors
// 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.
//go:build !noethtool
// SFP/QSFP module EEPROM parsing for Digital Optical Monitoring (DOM) /
// Digital Diagnostic Monitoring (DDM) data.
//
// Standards:
// - SFF-8472: SFP/SFP+ DDM (A0 + A2 EEPROM pages, 512 bytes total)
// - SFF-8636: QSFP/QSFP28 DOM (page 0, 256 bytes)
package collector
import (
"encoding/binary"
"fmt"
)
// SFP/QSFP module identifier values (EEPROM byte 0).
const (
sfpIdentifierSFP = 0x03 // SFP/SFP+/SFP28 (SFF-8472)
sfpIdentifierSFPAlt = 0x0B // SFP+ alternative identifier
sfpIdentifierQSFP = 0x0C // QSFP (SFF-8436)
sfpIdentifierQSFPP = 0x0D // QSFP+ (SFF-8436)
sfpIdentifierQSFP28 = 0x11 // QSFP28 (SFF-8636)
)
// sfpLaneMetrics holds per-lane optical monitoring values.
type sfpLaneMetrics struct {
txBias float64 // TX laser bias current in amperes
txPower float64 // TX optical power in watts
rxPower float64 // RX optical power in watts
}
// sfpMetrics holds parsed DOM/DDM values from a transceiver module.
type sfpMetrics struct {
temperature float64 // Module temperature in degrees Celsius
voltage float64 // Module supply voltage in volts
lanes []sfpLaneMetrics // Per-lane metrics (1 lane for SFP, 4 for QSFP)
}
// parseModuleEeprom parses raw EEPROM bytes returned by ethtool GMODULEEEPROM
// and extracts DOM/DDM values.
//
// Returns an error if the data is too short, the identifier is unrecognised, or DDM is not available.
func parseModuleEeprom(data []byte) (sfpMetrics, error) {
if len(data) < 1 {
return sfpMetrics{}, fmt.Errorf("module EEPROM data too short (%d bytes)", len(data))
}
switch data[0] {
case sfpIdentifierSFP, sfpIdentifierSFPAlt:
return parseSFF8472(data)
case sfpIdentifierQSFP, sfpIdentifierQSFPP, sfpIdentifierQSFP28:
return parseSFF8636(data)
default:
return sfpMetrics{}, fmt.Errorf("unsupported module identifier 0x%02x", data[0])
}
}
// parseSFF8472 parses SFP/SFP+ DDM data per SFF-8472.
func parseSFF8472(data []byte) (sfpMetrics, error) {
const (
a0DiagnosticType = 92 // A0 page: diagnostic monitoring type byte
ddmSupportBit = 0x40 // bit 6: DDM implemented
// Offsets within the full 512-byte dump (A2 page starts at 256).
a2PageOffset = 256
valuesOffset = a2PageOffset + 96
tempOffset = valuesOffset
voltageOffset = tempOffset + 2
txBiasOffset = voltageOffset + 2
txPowerOffset = txBiasOffset + 2
rxPowerOffset = txPowerOffset + 2
minLen = rxPowerOffset + 2
)
if len(data) < a0DiagnosticType+1 {
return sfpMetrics{}, fmt.Errorf("SFF-8472 EEPROM too short for diagnostic type byte (%d bytes)", len(data))
}
if data[a0DiagnosticType]&ddmSupportBit == 0 {
return sfpMetrics{}, fmt.Errorf("SFP module does not support DDM (diagnostic type byte: 0x%02x)", data[a0DiagnosticType])
}
if len(data) < minLen {
return sfpMetrics{}, fmt.Errorf("SFF-8472 EEPROM too short for DDM values (%d bytes, need %d)", len(data), minLen)
}
temp := parseSFPTemperature(data[tempOffset:])
voltage := parseSFPVoltage(data[voltageOffset:])
txBias := parseSFPBias(data[txBiasOffset:])
txPower := parseSFPPower(data[txPowerOffset:])
rxPower := parseSFPPower(data[rxPowerOffset:])
return sfpMetrics{
temperature: temp,
voltage: voltage,
lanes: []sfpLaneMetrics{
{txBias: txBias, txPower: txPower, rxPower: rxPower},
},
}, nil
}
// parseSFF8636 parses QSFP/QSFP28 DOM data per SFF-8636.
func parseSFF8636(data []byte) (sfpMetrics, error) {
// All real-time values are on Page 00h.
const (
// Table 6-8 Free Side Monitoring Values
tempOffset = 22 // Temperature MSB
voltageOffset = 26 // Supply voltage MSB
// Table 6-9 Channel Monitoring Values.
numLanes = 4
rxPowerOffset = 34 // RX power ch1 MSB
txBiasOffset = rxPowerOffset + numLanes*2 // TX bias ch1 MSB
txPowerOffset = txBiasOffset + numLanes*2 // TX power ch1 MSB
minLen = txPowerOffset + numLanes*2
)
if len(data) < minLen {
return sfpMetrics{}, fmt.Errorf("SFF-8636 EEPROM too short (%d bytes, need %d)", len(data), minLen)
}
temp := parseSFPTemperature(data[tempOffset:])
voltage := parseSFPVoltage(data[voltageOffset:])
lanes := make([]sfpLaneMetrics, numLanes)
for i := range numLanes {
lanes[i] = sfpLaneMetrics{
rxPower: parseSFPPower(data[rxPowerOffset+i*2:]),
txBias: parseSFPBias(data[txBiasOffset+i*2:]),
txPower: parseSFPPower(data[txPowerOffset+i*2:]),
}
}
return sfpMetrics{
temperature: temp,
voltage: voltage,
lanes: lanes,
}, nil
}
func parseSFPTemperature(b []byte) float64 {
// SFF-8472
//
// Table 9-1 Bit Weights (°C) for Temperature Reporting Registers
//
// +----------------------------------+----------------------------------+-------+-------+
// | Most Significant Byte (byte 96) | Least Significant Byte (byte 97) | | |
// +------+----+----+----+---+---+---+---+---+---+----+-----+-----+------+-------+-------+
// | D7 | D6 | D5 | D4 | D3| D2| D1| D0| D7| D6| D5 | D4 | D3 | D2 | D1 | D0 |
// +------+----+----+----+---+---+---+---+---+---+----+-----+-----+------+-------+-------+
// | Sign | 64 | 32 | 16 | 8 | 4 | 2 | 1 |1/2|1/4|1/8 |1/16 |1/32 | 1/64 | 1/128 | 1/256 |
// +------+----+----+----+---+---+---+---+---+---+----+-----+-----+------+-------+-------+
//
rawVal := int16(binary.BigEndian.Uint16(b))
return float64(rawVal) / 256.0
}
func parseSFPVoltage(b []byte) float64 {
// SFF-8472
//
// 9.2 Internal Calibration
//
// ...
// 2) Internally measured transceiver supply voltage. Represented as a 16-bit unsigned integer with the voltage
// defined as the full 16-bit value (0-65535) with LSB equal to 100 microvolts, yielding a total range of 0 V to +6.55 V.
rawVal := binary.BigEndian.Uint16(b)
mV := float64(rawVal) / 10
V := mV / 1000
return V
}
func parseSFPBias(b []byte) float64 {
// SFF-8472
//
// 9.2 Internal Calibration
//
// ...
// 3) Measured TX bias current in mA. Represented as a 16-bit unsigned integer with the current defined as the full
// 16-bit value (0-65535) with LSB equal to 2 microamps, yielding a total range of 0 to 131 mA.
rawVal := binary.BigEndian.Uint16(b)
mA := float64(rawVal) / 500
return mA
}
func parseSFPPower(b []byte) float64 {
// SFF-8472
//
// 9.2 Internal Calibration
//
// ...
// 4) Measured TX output power in mW. Represented as a 16-bit unsigned integer with the power defined as the
// full 16-bit value (0-65535) with LSB equal to 0.1 microwatts, yielding a total range of 0 to 6.5535 mW (-40 to +8.2 dBm).
// ...
// 5) Measured RX received optical power in mW. Value can represent either average received power or OMA
// depending upon how bit 3 of byte 92 (A0h) is set. Represented as a 16-bit unsigned integer with the power
// defined as the full 16-bit value (0-65535) with LSB equal to 0.1 microwatts, yielding a total range of 0 to 6.5535 mW (-40 to +8.2 dBm).
rawVal := binary.BigEndian.Uint16(b)
mW := float64(rawVal) / 10000
return mW
}

@ -0,0 +1,426 @@
// Copyright 2021 The Prometheus Authors
// 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.
//go:build !noethtool
package collector
import (
"encoding/binary"
"fmt"
"math"
"testing"
)
func almostEqual(a, b float64) bool {
if a == b {
return true
}
return math.Abs(a-b)/math.Max(math.Abs(a), math.Abs(b)) < 0.1
}
func makeSFF8472(temp int16, voltage, txBias, txPower, rxPower uint16, ddmByte byte) []byte {
data := make([]byte, 512)
data[0] = sfpIdentifierSFP
data[92] = ddmByte
binary.BigEndian.PutUint16(data[352:], uint16(temp))
binary.BigEndian.PutUint16(data[354:], voltage)
binary.BigEndian.PutUint16(data[356:], txBias)
binary.BigEndian.PutUint16(data[358:], txPower)
binary.BigEndian.PutUint16(data[360:], rxPower)
return data
}
func makeSFF8636(temp int16, voltage uint16, rxPower, txBias, txPower [4]uint16) []byte {
data := make([]byte, 58)
data[0] = sfpIdentifierQSFP28
binary.BigEndian.PutUint16(data[22:], uint16(temp))
binary.BigEndian.PutUint16(data[26:], voltage)
for i := range 4 {
binary.BigEndian.PutUint16(data[34+i*2:], rxPower[i])
binary.BigEndian.PutUint16(data[42+i*2:], txBias[i])
binary.BigEndian.PutUint16(data[50+i*2:], txPower[i])
}
return data
}
var sff8472Cases = []struct {
name string
data []byte
wantErr bool
wantTemp float64
wantVoltage float64
wantTxBias float64
wantTxPower float64
wantRxPower float64
}{
{
name: "typical values",
data: makeSFF8472(25*256, 33000, 10000, 10000, 5000, 0x40),
wantTemp: 25.0,
wantVoltage: 33000 * 100e-6,
wantTxBias: 20.0,
wantTxPower: 1.0,
wantRxPower: 0.5,
},
{
name: "negative temperature",
data: makeSFF8472(-10*256, 33000, 5000, 8000, 4000, 0x40),
wantTemp: -10.0,
wantVoltage: 33000 * 100e-6,
wantTxBias: 10.0,
wantTxPower: 0.8,
wantRxPower: 0.4,
},
{
name: "fractional temperature",
data: makeSFF8472(int16(25*256+128), 33000, 0, 0, 0, 0x40),
wantTemp: 25.5,
wantVoltage: 33000 * 100e-6,
},
{
name: "DDM not supported",
data: makeSFF8472(0, 0, 0, 0, 0, 0x00),
wantErr: true,
},
{
name: "too short for diagnostic type byte",
data: make([]byte, 50),
wantErr: true,
},
{
name: "too short for DDM values",
data: func() []byte {
d := make([]byte, 200)
d[0] = sfpIdentifierSFP
d[92] = 0x40
return d
}(),
wantErr: true,
},
{
name: "empty",
data: []byte{},
wantErr: true,
},
{
name: "zero values",
data: makeSFF8472(0, 0, 0, 0, 0, 0x40),
},
{
name: "alt SFP identifier (0x0B)",
data: func() []byte {
d := makeSFF8472(25*256, 33000, 10000, 10000, 5000, 0x40)
d[0] = sfpIdentifierSFPAlt
return d
}(),
wantTemp: 25.0,
wantVoltage: 33000 * 100e-6,
wantTxBias: 20.0,
wantTxPower: 1.0,
wantRxPower: 0.5,
},
}
var sff8636Cases = []struct {
name string
data []byte
wantErr bool
wantTemp float64
wantVoltage float64
wantLanes [4]sfpLaneMetrics
}{
{
name: "typical values",
data: makeSFF8636(
25*256, 33000,
[4]uint16{5000, 4800, 4900, 5100},
[4]uint16{10000, 9800, 10200, 10100},
[4]uint16{9000, 8800, 9100, 9200},
),
wantTemp: 25.0,
wantVoltage: 33000 * 100e-6,
wantLanes: [4]sfpLaneMetrics{
{txBias: 20.0, txPower: 0.9, rxPower: 0.5},
{txBias: 19.6, txPower: 0.88, rxPower: 0.48},
{txBias: 20.4, txPower: 0.91, rxPower: 0.49},
{txBias: 20.2, txPower: 0.92, rxPower: 0.51},
},
},
{
name: "negative temperature",
data: makeSFF8636(
-5*256, 33000,
[4]uint16{}, [4]uint16{}, [4]uint16{},
),
wantTemp: -5.0,
wantVoltage: 33000 * 100e-6,
},
{
name: "too short",
data: make([]byte, 10),
wantErr: true,
},
{
name: "empty",
data: []byte{},
wantErr: true,
},
{
name: "zero values",
data: makeSFF8636(0, 0, [4]uint16{}, [4]uint16{}, [4]uint16{}),
},
}
// moduleEepromCases are the table-driven test cases for parseModuleEeprom, also
// used as fuzzing seeds.
var moduleEepromCases = []struct {
name string
data []byte
wantErr bool
wantLanes int
}{
{
name: "SFP (0x03)",
data: makeSFF8472(25*256, 33000, 10000, 10000, 5000, 0x40),
wantLanes: 1,
},
{
name: "SFP alt (0x0B)",
data: func() []byte {
d := makeSFF8472(25*256, 33000, 10000, 10000, 5000, 0x40)
d[0] = sfpIdentifierSFPAlt
return d
}(),
wantLanes: 1,
},
{
name: "QSFP (0x0C)",
data: func() []byte {
d := makeSFF8636(25*256, 33000, [4]uint16{}, [4]uint16{}, [4]uint16{})
d[0] = sfpIdentifierQSFP
return d
}(),
wantLanes: 4,
},
{
name: "QSFP+ (0x0D)",
data: func() []byte {
d := makeSFF8636(25*256, 33000, [4]uint16{}, [4]uint16{}, [4]uint16{})
d[0] = sfpIdentifierQSFPP
return d
}(),
wantLanes: 4,
},
{
name: "QSFP28 (0x11)",
data: makeSFF8636(25*256, 33000, [4]uint16{}, [4]uint16{}, [4]uint16{}),
wantLanes: 4,
},
{
name: "unknown identifier",
data: []byte{0x01},
wantErr: true,
},
{
name: "empty",
data: []byte{},
wantErr: true,
},
}
func TestParseSFF8472(t *testing.T) {
for _, tc := range sff8472Cases {
t.Run(tc.name, func(t *testing.T) {
got, err := parseSFF8472(tc.data)
if tc.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !almostEqual(got.temperature, tc.wantTemp) {
t.Errorf("temperature: got %v, want %v", got.temperature, tc.wantTemp)
}
if !almostEqual(got.voltage, tc.wantVoltage) {
t.Errorf("voltage: got %v, want %v", got.voltage, tc.wantVoltage)
}
if len(got.lanes) != 1 {
t.Fatalf("expected 1 lane, got %d", len(got.lanes))
}
if !almostEqual(got.lanes[0].txBias, tc.wantTxBias) {
t.Errorf("txBias: got %v, want %v", got.lanes[0].txBias, tc.wantTxBias)
}
if !almostEqual(got.lanes[0].txPower, tc.wantTxPower) {
t.Errorf("txPower: got %v, want %v", got.lanes[0].txPower, tc.wantTxPower)
}
if !almostEqual(got.lanes[0].rxPower, tc.wantRxPower) {
t.Errorf("rxPower: got %v, want %v", got.lanes[0].rxPower, tc.wantRxPower)
}
})
}
}
func TestParseSFF8636(t *testing.T) {
for _, tc := range sff8636Cases {
t.Run(tc.name, func(t *testing.T) {
got, err := parseSFF8636(tc.data)
if tc.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !almostEqual(got.temperature, tc.wantTemp) {
t.Errorf("temperature: got %v, want %v", got.temperature, tc.wantTemp)
}
if !almostEqual(got.voltage, tc.wantVoltage) {
t.Errorf("voltage: got %v, want %v", got.voltage, tc.wantVoltage)
}
if len(got.lanes) != 4 {
t.Fatalf("expected 4 lanes, got %d", len(got.lanes))
}
for i, want := range tc.wantLanes {
l := got.lanes[i]
if !almostEqual(l.txBias, want.txBias) {
t.Errorf("lane %d txBias: got %v, want %v", i, l.txBias, want.txBias)
}
if !almostEqual(l.txPower, want.txPower) {
t.Errorf("lane %d txPower: got %v, want %v", i, l.txPower, want.txPower)
}
if !almostEqual(l.rxPower, want.rxPower) {
t.Errorf("lane %d rxPower: got %v, want %v", i, l.rxPower, want.rxPower)
}
}
})
}
}
func TestParseModuleEeprom(t *testing.T) {
for _, tc := range moduleEepromCases {
t.Run(tc.name, func(t *testing.T) {
got, err := parseModuleEeprom(tc.data)
if tc.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(got.lanes) != tc.wantLanes {
t.Errorf("lanes: got %d, want %d", len(got.lanes), tc.wantLanes)
}
})
}
}
func FuzzParseSFF8472(f *testing.F) {
for _, tc := range sff8472Cases {
f.Add(tc.data)
}
f.Fuzz(func(t *testing.T, data []byte) {
_, _ = parseSFF8472(data) //nolint:errcheck
})
}
func FuzzParseSFF8636(f *testing.F) {
for _, tc := range sff8636Cases {
f.Add(tc.data)
}
f.Fuzz(func(t *testing.T, data []byte) {
_, _ = parseSFF8636(data) //nolint:errcheck
})
}
func FuzzParseModuleEeprom(f *testing.F) {
for _, tc := range moduleEepromCases {
f.Add(tc.data)
}
f.Fuzz(func(t *testing.T, data []byte) {
_, _ = parseModuleEeprom(data) //nolint:errcheck
})
}
func Test_parseSFPTemperature(t *testing.T) {
// Values from Table 9-4 TEC Current Format.
tests := []struct {
b [2]byte
want float64 // celsius
}{
{[2]byte{0x7D, 0}, 125.0},
{[2]byte{0x19, 0}, 25.0},
{[2]byte{0x00, 0xFF}, 1},
{[2]byte{0x00, 0x01}, 0.004},
{[2]byte{}, 0},
{[2]byte{0xFF, 0xFF}, -0.004},
{[2]byte{0xE7, 0x00}, -25.0},
{[2]byte{0xD8, 0x00}, -40.0},
}
for i, tt := range tests {
tt := tt
t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
got := parseSFPTemperature(tt.b[:])
if !almostEqual(tt.want, got) {
t.Fatalf("Expected ~ %v, got %v", tt.want, got)
}
})
}
}
func Test_parseSFPVoltage(t *testing.T) {
tests := []struct {
b [2]byte
want float64 // volts
}{
{[2]byte{0xFF, 0xFF}, 6.55},
{[2]byte{0x00, 0x01}, 0.0001},
{[2]byte{}, 0},
}
for i, tt := range tests {
tt := tt
t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
got := parseSFPVoltage(tt.b[:])
if !almostEqual(tt.want, got) {
t.Fatalf("Expected ~ %v, got %v", tt.want, got)
}
})
}
}
func Test_parseSFPPower(t *testing.T) {
tests := []struct {
b [2]byte
want float64 // milliwatts
}{
{[2]byte{0xFF, 0xFF}, 6.55},
{[2]byte{0x00, 0x01}, 0.0001},
{[2]byte{}, 0},
}
for i, tt := range tests {
tt := tt
t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
got := parseSFPPower(tt.b[:])
if !almostEqual(tt.want, got) {
t.Fatalf("Expected ~ %v, got %v", tt.want, got)
}
})
}
}
Loading…
Cancel
Save