package faultRecordFile import ( "archive/zip" "bytes" "os" "strings" "testing" proto "fwh-toolbox-server/function/FaultRecordProtocol" ) const ( sampleArchive = "../../../docs/fault-record/2026-10-14_05_46_20.tar.bz2" // 2026-10-14 样本 379(cmd=0, code=3) 的 hexData sampleDirectoryHex = "0F0107E801010000220000000110020207EA070E0235230000000110010307EA080C0203050000000110010407EA080D1528280000000110010507EA080E0318100000000110010607EA0810033A050000000110010707EA08120303080000000110010807E90B18010B2E0000000110010907E90B1A152C060000000110010A07E705041601030000000110020B07E705041602210000000110020C07E705041603280000000110020D07E7050416042E0000000110020E07E70504161C0E0000000110020F07EA061D0F0723000000011001" ) func TestParseDirectory(t *testing.T) { v, err := ParseRealtime(3, sampleDirectoryHex) if err != nil { t.Fatal(err) } list := v.([]DirectoryEntry) if len(list) != 15 { t.Fatalf("entries = %d", len(list)) } e := list[6] if e.Index != 7 || e.Time != "2026-08-18 03:03:08" || e.TriggerSource != 0x1001 || e.TriggerSourceName == "" || e.ParamListVersion != "00000001" { t.Errorf("entry 7: %+v", e) } if _, err := ParseRealtime(3, sampleDirectoryHex[:40]); err == nil { t.Error("truncated directory should fail") } } func TestParseBasicInfo(t *testing.T) { b, err := ParseRealtime(2, "01010000000111941194060004000200") if err != nil { t.Fatal(err) } bi := b.(BasicInfo) if bi.SampleFrequency != 4500 || bi.TotalPoints != 1536 || bi.PrePoints != 1024 || bi.PostPoints != 512 { t.Errorf("basic info: %+v", bi) } } func loadSampleArchive(t *testing.T) []byte { t.Helper() data, err := os.ReadFile(sampleArchive) if err != nil { t.Skipf("sample archive not found: %v", err) } return data } func TestParseSampleArchive(t *testing.T) { records, err := ParseArchive(loadSampleArchive(t), proto.DeviceTypeAGate) if err != nil { t.Fatal(err) } if len(records) != 1 { t.Fatalf("records = %d", len(records)) } rec := records[0] if rec.Batch != "2026-10-14_05_46_20" || rec.Sn != "AFCI_TEST_0713" || rec.RecordIndex != 7 { t.Errorf("record identity: %s %s %d", rec.Batch, rec.Sn, rec.RecordIndex) } f := rec.Failure if f.DeclaredLength != 512 || f.ParsedBytes != 308 { t.Errorf("length: declared=%d parsed=%d", f.DeclaredLength, f.ParsedBytes) } if f.Index != 7 || f.Time != "2026-08-18 03:03:08" || f.TriggerSource != 0x1001 { t.Errorf("header: %+v", f) } if f.Basic.SampleFrequency != 4500 || f.Basic.TotalPoints != 1536 || f.Basic.PrePoints != 1024 { t.Errorf("basic: %+v", f.Basic) } if f.DcdcVersion != "EMS_SL:V10R80B21D00" || f.InvVersion != "" || f.FpgaVersion != "" { t.Errorf("versions: %q %q %q", f.DcdcVersion, f.InvVersion, f.FpgaVersion) } if len(f.Snapshots) != 6 || *f.Snapshots[0].Value != 2049 || len(f.SampleMeanings) != 16 || len(f.ConfigSamples) != 4 { t.Errorf("channels: snap=%d samp=%d cfg=%d", len(f.Snapshots), len(f.SampleMeanings), len(f.ConfigSamples)) } w := rec.Waveform if len(w.Channels) != 16 || w.Points != 1536 { t.Fatalf("waveform shape: %d × %d", len(w.Channels), w.Points) } // 故障字通道在第 1024 点跳变 ch0 := w.Channels[0].Values if ch0[1023] != 0 || ch0[1024] != 0x0801 { t.Errorf("ch0 around fault: %d %d", ch0[1023], ch0[1024]) } // 正弦通道按 int16 解释,含负值 if w.Channels[4].Values[0] != -396 { t.Errorf("ch4[0] = %d", w.Channels[4].Values[0]) } if w.Channels[12].Configurable == nil || w.Channels[12].Configurable.Address != 0x14990 || w.Channels[11].Configurable != nil { t.Error("configurable channels should be the last 4") } if w.TimeOffsetMs(1024) != 0 { t.Errorf("fault time offset: %v", w.TimeOffsetMs(1024)) } } func TestPEKeepsVersions(t *testing.T) { files, err := ExtractArchive(loadSampleArchive(t)) if err != nil { t.Fatal(err) } rec, err := ParseRecordFiles(files[0], proto.DeviceTypePE) if err != nil { t.Fatal(err) } if rec.Failure.InvVersion == "" { t.Error("pe should parse INV version field") } } func TestBuildCSVZip(t *testing.T) { records, err := ParseArchive(loadSampleArchive(t), proto.DeviceTypeAGate) if err != nil { t.Fatal(err) } data, err := BuildCSVZip(records) if err != nil { t.Fatal(err) } zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) if err != nil { t.Fatal(err) } if len(zr.File) != 2 { t.Fatalf("zip files = %d", len(zr.File)) } var wf bytes.Buffer if err := WriteWaveformCSV(&wf, records[0]); err != nil { t.Fatal(err) } lines := strings.Split(strings.TrimSpace(wf.String()), "\n") if len(lines) != 1537 { t.Errorf("waveform csv lines = %d", len(lines)) } if !strings.HasPrefix(lines[1025], "1024,0.000,2049,") { t.Errorf("fault row: %s", lines[1025]) } } func TestRejectBadArchive(t *testing.T) { if _, err := ParseArchive([]byte("not an archive"), proto.DeviceTypeAGate); err == nil { t.Error("garbage should fail") } } func TestConfigurableChannelModes(t *testing.T) { // code=1:最大/当前记录数、两个时间、快照/采样/可配快照为空,可配采样 2 项(默认方式 Uint16、代码方式 0x1234) hexData := "1005" + "07EA0A100C0000" + "07EA0A100C0000" + "0000" + "0000" + "0000" + "0202" + "0001" + "00014990" + "0010" + "12340000" v, err := ParseRealtime(1, hexData) if err != nil { t.Fatal(err) } cs := v.(Overview).ConfigSamples if len(cs) != 2 || cs[0].Mode != 0 || cs[0].TypeName != "Uint16" || cs[0].Address != 0x14990 || cs[1].Mode != 1 || ConfigurableDesc(cs[1]) != "代码0x1234" { t.Errorf("configurable: %+v", cs) } }