Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
475f6ac472 | ||
|
|
93ce676f04 |
Submodule internal/chart updated: c025ae0477...2fb01d30a6
@@ -2810,13 +2810,6 @@ func shouldCrawlPath(path string) bool {
|
||||
if isAllowedNVSwitchFabricPath(normalized) {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(normalized, "/Chassis/") &&
|
||||
strings.Contains(normalized, "/PCIeDevices/") &&
|
||||
strings.Contains(normalized, "/PCIeFunctions/") {
|
||||
// Chassis-level PCIeFunctions links are frequently noisy/slow on some BMCs
|
||||
// and duplicate data we already collect from PCIe devices/functions elsewhere.
|
||||
return false
|
||||
}
|
||||
if strings.Contains(normalized, "/Memory/") {
|
||||
after := strings.SplitN(normalized, "/Memory/", 2)
|
||||
if len(after) == 2 && strings.Count(after[1], "/") >= 1 {
|
||||
@@ -3867,23 +3860,31 @@ func parseNIC(doc map[string]interface{}) models.NetworkAdapter {
|
||||
var linkSpeed string
|
||||
var maxLinkSpeed string
|
||||
if controllers, ok := doc["Controllers"].([]interface{}); ok && len(controllers) > 0 {
|
||||
if ctrl, ok := controllers[0].(map[string]interface{}); ok {
|
||||
totalPortCount := 0
|
||||
for _, ctrlAny := range controllers {
|
||||
ctrl, ok := ctrlAny.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ctrlLocation := redfishLocationLabel(ctrl["Location"])
|
||||
location = firstNonEmpty(location, ctrlLocation)
|
||||
if isWeakRedfishNICSlotLabel(slot) {
|
||||
slot = firstNonEmpty(ctrlLocation, slot)
|
||||
}
|
||||
firmware = asString(ctrl["FirmwarePackageVersion"])
|
||||
if caps, ok := ctrl["ControllerCapabilities"].(map[string]interface{}); ok {
|
||||
portCount = sanitizeNetworkPortCount(asInt(caps["NetworkPortCount"]))
|
||||
if normalizeRedfishIdentityField(firmware) == "" {
|
||||
firmware = findFirstNormalizedStringByKeys(ctrl, "FirmwarePackageVersion", "FirmwareVersion")
|
||||
}
|
||||
if pcieIf, ok := ctrl["PCIeInterface"].(map[string]interface{}); ok {
|
||||
if caps, ok := ctrl["ControllerCapabilities"].(map[string]interface{}); ok {
|
||||
totalPortCount += sanitizeNetworkPortCount(asInt(caps["NetworkPortCount"]))
|
||||
}
|
||||
if pcieIf, ok := ctrl["PCIeInterface"].(map[string]interface{}); ok && linkWidth == 0 && maxLinkWidth == 0 && linkSpeed == "" && maxLinkSpeed == "" {
|
||||
linkWidth = asInt(pcieIf["LanesInUse"])
|
||||
maxLinkWidth = asInt(pcieIf["MaxLanes"])
|
||||
linkSpeed = firstNonEmpty(asString(pcieIf["PCIeType"]), asString(pcieIf["CurrentLinkSpeedGTs"]), asString(pcieIf["CurrentLinkSpeed"]))
|
||||
maxLinkSpeed = firstNonEmpty(asString(pcieIf["MaxPCIeType"]), asString(pcieIf["MaxLinkSpeedGTs"]), asString(pcieIf["MaxLinkSpeed"]))
|
||||
}
|
||||
}
|
||||
portCount = sanitizeNetworkPortCount(totalPortCount)
|
||||
}
|
||||
|
||||
return models.NetworkAdapter{
|
||||
@@ -3913,10 +3914,14 @@ func isWeakRedfishNICSlotLabel(slot string) bool {
|
||||
if slot == "" {
|
||||
return true
|
||||
}
|
||||
lower := strings.ToLower(slot)
|
||||
if isNumericString(slot) {
|
||||
return true
|
||||
}
|
||||
if strings.EqualFold(slot, "nic") || strings.HasPrefix(strings.ToLower(slot), "nic") && !strings.Contains(strings.ToLower(slot), "slot") {
|
||||
if strings.EqualFold(slot, "nic") || strings.HasPrefix(lower, "nic") && !strings.Contains(lower, "slot") {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(lower, "devtype") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -3956,6 +3961,16 @@ func enrichNICFromPCIe(nic *models.NetworkAdapter, pcieDoc map[string]interface{
|
||||
if nic == nil {
|
||||
return
|
||||
}
|
||||
pcieSlot := redfishLocationLabel(pcieDoc["Slot"])
|
||||
if pcieSlot == "" {
|
||||
pcieSlot = redfishLocationLabel(pcieDoc["Location"])
|
||||
}
|
||||
if isWeakRedfishNICSlotLabel(nic.Slot) && pcieSlot != "" {
|
||||
nic.Slot = pcieSlot
|
||||
}
|
||||
if strings.TrimSpace(nic.Location) == "" && pcieSlot != "" {
|
||||
nic.Location = pcieSlot
|
||||
}
|
||||
if strings.TrimSpace(nic.BDF) == "" {
|
||||
nic.BDF = firstNonEmpty(asString(pcieDoc["BDF"]), buildBDFfromOemPublic(pcieDoc))
|
||||
}
|
||||
@@ -3977,6 +3992,15 @@ func enrichNICFromPCIe(nic *models.NetworkAdapter, pcieDoc map[string]interface{
|
||||
if strings.TrimSpace(nic.MaxLinkSpeed) == "" {
|
||||
nic.MaxLinkSpeed = firstNonEmpty(asString(pcieDoc["MaxLinkSpeedGTs"]), asString(pcieDoc["MaxLinkSpeed"]))
|
||||
}
|
||||
if normalizeRedfishIdentityField(nic.SerialNumber) == "" {
|
||||
nic.SerialNumber = findFirstNormalizedStringByKeys(pcieDoc, "SerialNumber")
|
||||
}
|
||||
if normalizeRedfishIdentityField(nic.PartNumber) == "" {
|
||||
nic.PartNumber = findFirstNormalizedStringByKeys(pcieDoc, "PartNumber", "ProductPartNumber")
|
||||
}
|
||||
if normalizeRedfishIdentityField(nic.Firmware) == "" {
|
||||
nic.Firmware = findFirstNormalizedStringByKeys(pcieDoc, "FirmwareVersion", "FirmwarePackageVersion")
|
||||
}
|
||||
for _, fn := range functionDocs {
|
||||
if strings.TrimSpace(nic.BDF) == "" {
|
||||
nic.BDF = sanitizeRedfishBDF(asString(fn["FunctionId"]))
|
||||
@@ -3999,6 +4023,15 @@ func enrichNICFromPCIe(nic *models.NetworkAdapter, pcieDoc map[string]interface{
|
||||
if strings.TrimSpace(nic.MaxLinkSpeed) == "" {
|
||||
nic.MaxLinkSpeed = firstNonEmpty(asString(fn["MaxLinkSpeedGTs"]), asString(fn["MaxLinkSpeed"]))
|
||||
}
|
||||
if normalizeRedfishIdentityField(nic.SerialNumber) == "" {
|
||||
nic.SerialNumber = findFirstNormalizedStringByKeys(fn, "SerialNumber")
|
||||
}
|
||||
if normalizeRedfishIdentityField(nic.PartNumber) == "" {
|
||||
nic.PartNumber = findFirstNormalizedStringByKeys(fn, "PartNumber", "ProductPartNumber")
|
||||
}
|
||||
if normalizeRedfishIdentityField(nic.Firmware) == "" {
|
||||
nic.Firmware = findFirstNormalizedStringByKeys(fn, "FirmwareVersion", "FirmwarePackageVersion")
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(nic.Vendor) == "" {
|
||||
nic.Vendor = pciids.VendorName(nic.VendorID)
|
||||
|
||||
@@ -1197,6 +1197,8 @@ func TestEnrichNICFromPCIeFunctions(t *testing.T) {
|
||||
"FunctionId": "0000:17:00.0",
|
||||
"VendorId": "0x15b3",
|
||||
"DeviceId": "0x1021",
|
||||
"SerialNumber": "MT-SN-0001",
|
||||
"PartNumber": "MCX623106AC-CDAT",
|
||||
"CurrentLinkWidth": 16,
|
||||
"CurrentLinkSpeedGTs": "32 GT/s",
|
||||
"MaxLinkWidth": 16,
|
||||
@@ -1214,6 +1216,12 @@ func TestEnrichNICFromPCIeFunctions(t *testing.T) {
|
||||
if nic.BDF != "0000:17:00.0" {
|
||||
t.Fatalf("unexpected NIC BDF: %q", nic.BDF)
|
||||
}
|
||||
if nic.SerialNumber != "NIC-SN-1" {
|
||||
t.Fatalf("expected existing NIC serial to be preserved, got %q", nic.SerialNumber)
|
||||
}
|
||||
if nic.PartNumber != "MCX623106AC-CDAT" {
|
||||
t.Fatalf("expected NIC part number from PCIe function, got %q", nic.PartNumber)
|
||||
}
|
||||
if nic.LinkWidth != 16 || nic.MaxLinkWidth != 16 {
|
||||
t.Fatalf("unexpected NIC link width state: current=%d max=%d", nic.LinkWidth, nic.MaxLinkWidth)
|
||||
}
|
||||
@@ -1222,6 +1230,63 @@ func TestEnrichNICFromPCIeFunctions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnrichNICFromPCIeFunctions_FillsMissingIdentityFromFunctionDoc(t *testing.T) {
|
||||
nic := parseNIC(map[string]interface{}{
|
||||
"Id": "DevType7_NIC1",
|
||||
"Controllers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"ControllerCapabilities": map[string]interface{}{
|
||||
"NetworkPortCount": 1,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"ControllerCapabilities": map[string]interface{}{
|
||||
"NetworkPortCount": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
pcieDoc := map[string]interface{}{
|
||||
"Slot": map[string]interface{}{
|
||||
"Location": map[string]interface{}{
|
||||
"PartLocation": map[string]interface{}{
|
||||
"ServiceLabel": "RISER4",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
functionDocs := []map[string]interface{}{
|
||||
{
|
||||
"FunctionId": "0000:0f:00.0",
|
||||
"VendorId": "0x15b3",
|
||||
"DeviceId": "0x101f",
|
||||
"SerialNumber": "MT2412X00001",
|
||||
"PartNumber": "MCX623432AC-GDA_Ax",
|
||||
},
|
||||
}
|
||||
|
||||
enrichNICFromPCIe(&nic, pcieDoc, functionDocs, nil)
|
||||
if nic.Slot != "RISER4" {
|
||||
t.Fatalf("expected slot from PCIe slot label, got %q", nic.Slot)
|
||||
}
|
||||
if nic.Location != "RISER4" {
|
||||
t.Fatalf("expected location from PCIe slot label, got %q", nic.Location)
|
||||
}
|
||||
if nic.PortCount != 2 {
|
||||
t.Fatalf("expected combined port count from controllers, got %d", nic.PortCount)
|
||||
}
|
||||
if nic.SerialNumber != "MT2412X00001" {
|
||||
t.Fatalf("expected serial from PCIe function, got %q", nic.SerialNumber)
|
||||
}
|
||||
if nic.PartNumber != "MCX623432AC-GDA_Ax" {
|
||||
t.Fatalf("expected part number from PCIe function, got %q", nic.PartNumber)
|
||||
}
|
||||
if nic.BDF != "0000:0f:00.0" {
|
||||
t.Fatalf("expected BDF from PCIe function, got %q", nic.BDF)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNIC_PortCountFromControllerCapabilities(t *testing.T) {
|
||||
nic := parseNIC(map[string]interface{}{
|
||||
"Id": "1",
|
||||
@@ -3462,8 +3527,8 @@ func TestShouldCrawlPath_MemoryAndProcessorMetricsAreAllowed(t *testing.T) {
|
||||
if !shouldCrawlPath("/redfish/v1/Systems/1/Processors/CPU0/ProcessorMetrics") {
|
||||
t.Fatalf("expected CPU metrics subresource to be crawlable")
|
||||
}
|
||||
if shouldCrawlPath("/redfish/v1/Chassis/1/PCIeDevices/0/PCIeFunctions/1") {
|
||||
t.Fatalf("expected noisy chassis pciefunctions branch to be skipped")
|
||||
if !shouldCrawlPath("/redfish/v1/Chassis/1/PCIeDevices/0/PCIeFunctions/1") {
|
||||
t.Fatalf("expected chassis pciefunctions resource to be crawlable for NIC/GPU identity recovery")
|
||||
}
|
||||
if !shouldCrawlPath("/redfish/v1/Fabrics/HGX_NVLinkFabric_0/Switches/NVSwitch_0") {
|
||||
t.Fatalf("expected NVSwitch fabric resource to be crawlable")
|
||||
|
||||
@@ -358,10 +358,12 @@ func dedupeCanonicalDevices(items []models.HardwareDevice) []models.HardwareDevi
|
||||
prev.score = canonicalScore(prev.item)
|
||||
byKey[key] = prev
|
||||
}
|
||||
// Secondary pass: for items without serial/BDF (noKey), try to merge into an
|
||||
// existing keyed entry with the same model+manufacturer. This handles the case
|
||||
// where a device appears both in PCIeDevices (with BDF) and NetworkAdapters
|
||||
// (without BDF) — e.g. Inspur outboardPCIeCard vs PCIeCard with the same model.
|
||||
// Secondary pass: for PCIe-class items without serial/BDF (noKey), try to merge
|
||||
// into an existing keyed entry with the same model+manufacturer. This handles
|
||||
// the case where a device appears both in PCIeDevices (with BDF) and
|
||||
// NetworkAdapters (without BDF) — e.g. Inspur outboardPCIeCard vs PCIeCard
|
||||
// with the same model. Do not apply this to storage: repeated NVMe slots often
|
||||
// share the same model string and would collapse incorrectly.
|
||||
// deviceIdentity returns the best available model name for secondary matching,
|
||||
// preferring Model over DeviceClass (which may hold a resolved device name).
|
||||
deviceIdentity := func(d models.HardwareDevice) string {
|
||||
@@ -377,6 +379,10 @@ func dedupeCanonicalDevices(items []models.HardwareDevice) []models.HardwareDevi
|
||||
var unmatched []models.HardwareDevice
|
||||
for _, item := range noKey {
|
||||
mergeKind := canonicalMergeKind(item.Kind)
|
||||
if mergeKind != "pcie-class" {
|
||||
unmatched = append(unmatched, item)
|
||||
continue
|
||||
}
|
||||
identity := deviceIdentity(item)
|
||||
mfr := strings.ToLower(strings.TrimSpace(item.Manufacturer))
|
||||
if identity == "" {
|
||||
@@ -721,18 +727,16 @@ func convertStorageFromDevices(devices []models.HardwareDevice, collectedAt stri
|
||||
if isVirtualExportStorageDevice(d) {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(d.SerialNumber) == "" {
|
||||
continue
|
||||
}
|
||||
present := d.Present == nil || *d.Present
|
||||
if !present {
|
||||
if !shouldExportStorageDevice(d) {
|
||||
continue
|
||||
}
|
||||
present := boolFromPresentPtr(d.Present, true)
|
||||
status := inferStorageStatus(models.Storage{Present: present})
|
||||
if strings.TrimSpace(d.Status) != "" {
|
||||
status = normalizeStatus(d.Status, false)
|
||||
status = normalizeStatus(d.Status, !present)
|
||||
}
|
||||
meta := buildStatusMeta(status, d.StatusCheckedAt, d.StatusChangedAt, d.StatusHistory, d.ErrorDescription, collectedAt)
|
||||
presentValue := present
|
||||
result = append(result, ReanimatorStorage{
|
||||
Slot: d.Slot,
|
||||
Type: d.Type,
|
||||
@@ -742,6 +746,7 @@ func convertStorageFromDevices(devices []models.HardwareDevice, collectedAt stri
|
||||
Manufacturer: d.Manufacturer,
|
||||
Firmware: d.Firmware,
|
||||
Interface: d.Interface,
|
||||
Present: &presentValue,
|
||||
TemperatureC: floatFromDetailMap(d.Details, "temperature_c"),
|
||||
PowerOnHours: int64FromDetailMap(d.Details, "power_on_hours"),
|
||||
PowerCycles: int64FromDetailMap(d.Details, "power_cycles"),
|
||||
@@ -1386,14 +1391,16 @@ func convertStorage(storage []models.Storage, collectedAt string) []ReanimatorSt
|
||||
|
||||
result := make([]ReanimatorStorage, 0, len(storage))
|
||||
for _, stor := range storage {
|
||||
// Skip storage without serial number
|
||||
if stor.SerialNumber == "" {
|
||||
if isVirtualLegacyStorageDevice(stor) {
|
||||
continue
|
||||
}
|
||||
if !shouldExportLegacyStorage(stor) {
|
||||
continue
|
||||
}
|
||||
|
||||
status := inferStorageStatus(stor)
|
||||
if strings.TrimSpace(stor.Status) != "" {
|
||||
status = normalizeStatus(stor.Status, false)
|
||||
status = normalizeStatus(stor.Status, !stor.Present)
|
||||
}
|
||||
meta := buildStatusMeta(
|
||||
status,
|
||||
@@ -1403,6 +1410,7 @@ func convertStorage(storage []models.Storage, collectedAt string) []ReanimatorSt
|
||||
stor.ErrorDescription,
|
||||
collectedAt,
|
||||
)
|
||||
present := stor.Present
|
||||
|
||||
result = append(result, ReanimatorStorage{
|
||||
Slot: stor.Slot,
|
||||
@@ -1413,6 +1421,7 @@ func convertStorage(storage []models.Storage, collectedAt string) []ReanimatorSt
|
||||
Manufacturer: stor.Manufacturer,
|
||||
Firmware: stor.Firmware,
|
||||
Interface: stor.Interface,
|
||||
Present: &present,
|
||||
RemainingEndurancePct: stor.RemainingEndurancePct,
|
||||
Status: status,
|
||||
StatusCheckedAt: meta.StatusCheckedAt,
|
||||
@@ -1424,6 +1433,53 @@ func convertStorage(storage []models.Storage, collectedAt string) []ReanimatorSt
|
||||
return result
|
||||
}
|
||||
|
||||
func shouldExportStorageDevice(d models.HardwareDevice) bool {
|
||||
if normalizedSerial(d.SerialNumber) != "" {
|
||||
return true
|
||||
}
|
||||
if strings.TrimSpace(d.Slot) != "" {
|
||||
return true
|
||||
}
|
||||
if hasMeaningfulExporterText(d.Model) {
|
||||
return true
|
||||
}
|
||||
if hasMeaningfulExporterText(d.Type) || hasMeaningfulExporterText(d.Interface) {
|
||||
return true
|
||||
}
|
||||
if d.SizeGB > 0 {
|
||||
return true
|
||||
}
|
||||
return d.Present != nil
|
||||
}
|
||||
|
||||
func shouldExportLegacyStorage(stor models.Storage) bool {
|
||||
if normalizedSerial(stor.SerialNumber) != "" {
|
||||
return true
|
||||
}
|
||||
if strings.TrimSpace(stor.Slot) != "" {
|
||||
return true
|
||||
}
|
||||
if hasMeaningfulExporterText(stor.Model) {
|
||||
return true
|
||||
}
|
||||
if hasMeaningfulExporterText(stor.Type) || hasMeaningfulExporterText(stor.Interface) {
|
||||
return true
|
||||
}
|
||||
if stor.SizeGB > 0 {
|
||||
return true
|
||||
}
|
||||
return stor.Present
|
||||
}
|
||||
|
||||
func isVirtualLegacyStorageDevice(stor models.Storage) bool {
|
||||
return isVirtualExportStorageDevice(models.HardwareDevice{
|
||||
Kind: models.DeviceKindStorage,
|
||||
Slot: stor.Slot,
|
||||
Model: stor.Model,
|
||||
Manufacturer: stor.Manufacturer,
|
||||
})
|
||||
}
|
||||
|
||||
// convertPCIeDevices converts PCIe devices, GPUs, and network adapters to Reanimator format
|
||||
func convertPCIeDevices(hw *models.HardwareConfig, collectedAt string) []ReanimatorPCIe {
|
||||
result := make([]ReanimatorPCIe, 0)
|
||||
|
||||
@@ -447,20 +447,26 @@ func TestConvertStorage(t *testing.T) {
|
||||
Slot: "OB02",
|
||||
Type: "NVMe",
|
||||
Model: "INTEL SSDPF2KX076T1",
|
||||
SerialNumber: "", // No serial - should be skipped
|
||||
SerialNumber: "",
|
||||
Present: true,
|
||||
},
|
||||
}
|
||||
|
||||
result := convertStorage(storage, "2026-02-10T15:30:00Z")
|
||||
|
||||
if len(result) != 1 {
|
||||
t.Fatalf("expected 1 storage device (skipped one without serial), got %d", len(result))
|
||||
if len(result) != 2 {
|
||||
t.Fatalf("expected both inventory slots to be exported, got %d", len(result))
|
||||
}
|
||||
|
||||
if result[0].Status != "Unknown" {
|
||||
t.Errorf("expected Unknown status, got %q", result[0].Status)
|
||||
}
|
||||
if result[1].SerialNumber != "" {
|
||||
t.Errorf("expected empty serial for second storage slot, got %q", result[1].SerialNumber)
|
||||
}
|
||||
if result[1].Present == nil || !*result[1].Present {
|
||||
t.Fatalf("expected present=true to be preserved for populated slot without serial")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_SkipsAMIVirtualStorageDevices(t *testing.T) {
|
||||
@@ -994,6 +1000,52 @@ func TestConvertToReanimator_StatusFallbackUsesCollectedAt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_ExportsStorageInventoryWithoutSerial(t *testing.T) {
|
||||
collectedAt := time.Date(2026, 4, 1, 9, 0, 0, 0, time.UTC)
|
||||
input := &models.AnalysisResult{
|
||||
Filename: "nvme-inventory.json",
|
||||
CollectedAt: collectedAt,
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
||||
Storage: []models.Storage{
|
||||
{
|
||||
Slot: "OB01",
|
||||
Type: "NVMe",
|
||||
Model: "PM9A3",
|
||||
SerialNumber: "SSD-001",
|
||||
Present: true,
|
||||
},
|
||||
{
|
||||
Slot: "OB02",
|
||||
Type: "NVMe",
|
||||
Model: "PM9A3",
|
||||
Present: true,
|
||||
},
|
||||
{
|
||||
Slot: "OB03",
|
||||
Type: "NVMe",
|
||||
Model: "PM9A3",
|
||||
Present: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ConvertToReanimator(input)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
||||
}
|
||||
if len(out.Hardware.Storage) != 3 {
|
||||
t.Fatalf("expected 3 storage entries including inventory slots without serial, got %d", len(out.Hardware.Storage))
|
||||
}
|
||||
if out.Hardware.Storage[1].Slot != "OB02" || out.Hardware.Storage[1].SerialNumber != "" {
|
||||
t.Fatalf("expected OB02 storage slot without serial to survive export, got %#v", out.Hardware.Storage[1])
|
||||
}
|
||||
if out.Hardware.Storage[2].Present == nil || *out.Hardware.Storage[2].Present {
|
||||
t.Fatalf("expected OB03 to preserve present=false, got %#v", out.Hardware.Storage[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_FirmwareExcludesDeviceBoundEntries(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Filename: "fw-filter-test.json",
|
||||
|
||||
Reference in New Issue
Block a user