package faultRecordProtocol import ( "encoding/csv" "encoding/json" "os" "strings" "testing" ) const sampleLog = "../../../docs/fault-record/CloudLog_2026-10-14_104644.csv" // loadSampleMessages 读取 2026-10-14 样本日志,按 cmdType 分组返回原始报文 func loadSampleMessages(t *testing.T) map[uint][][]byte { t.Helper() f, err := os.Open(sampleLog) if err != nil { t.Skipf("sample log not found: %v", err) } defer f.Close() rows, err := csv.NewReader(f).ReadAll() if err != nil { t.Fatal(err) } msgs := map[uint][][]byte{} for _, row := range rows[1:] { raw := []byte(row[7]) // logData cmdType, _, _, err := SplitEnvelope(raw) if err != nil { t.Fatalf("split envelope: %v", err) } msgs[cmdType] = append(msgs[cmdType], raw) } return msgs } func TestSampleMessages(t *testing.T) { msgs := loadSampleMessages(t) for _, raw := range msgs[CmdTypeAck] { _, _, da, _ := SplitEnvelope(raw) ack, err := ParseAck(da) if err != nil || !ack.Ok() { t.Errorf("378 parse: %+v %v", ack, err) } } var gotDir, gotRecord bool for _, raw := range msgs[CmdTypeReply] { _, equipNo, da, _ := SplitEnvelope(raw) r, err := ParseReply(da) if err != nil || !r.Ok() { t.Fatalf("379 parse: %+v %v", r, err) } info, ok := r.FindInfo(DeviceTypeAGate, equipNo) if !ok { t.Fatal("379 info not found") } switch r.Cmd { case CmdRealtime: gotDir = len(info.HexList) == 1 && info.HexList[0].Code == 3 && len(info.HexList[0].HexData) == 422 case CmdGetRecord: gotRecord = r.Filename != "" && len(info.IndexInfo) == 2 && info.IndexInfo[0].Index == 4 && info.IndexInfo[1].Index == 5 } } if !gotDir || !gotRecord { t.Errorf("379 content: dir=%v record=%v", gotDir, gotRecord) } _, _, da, _ := SplitEnvelope(msgs[CmdTypeUploadCall][0]) call, err := ParseUploadCall(da) if err != nil || call.FaultRecordID == 0 || call.FileName == "" { t.Errorf("607 parse: %+v %v", call, err) } // 旧版本 608 的第二个 result 实为 id,应等于同批次 607.faultRecordID ids := map[string]int64{} for _, raw := range msgs[CmdTypeUploadCall] { _, _, da, _ := SplitEnvelope(raw) c, _ := ParseUploadCall(da) ids[c.FileName] = c.FaultRecordID } for _, raw := range msgs[CmdTypeUploadAck] { _, _, da, _ := SplitEnvelope(raw) ack, err := ParseUploadAck(da) if err != nil || ack.Result != 0 || ack.Filename == "" || ack.Id == 0 || ack.Id != ids[ack.Filename] { t.Errorf("608 parse: %+v %v", ack, err) } } for _, raw := range msgs[CmdTypeUploadDone] { _, _, da, _ := SplitEnvelope(raw) done, err := ParseUploadDone(da) if err != nil || !done.Ok() || BatchFromUploadFileName(done.FileName) == done.FileName { t.Errorf("609 parse: %+v %v", done, err) } } } func TestBuildRequests(t *testing.T) { req, err := NewGetRecordRequest("agate", "AFCI_TEST_0713", []int{7}) if err != nil { t.Fatal(err) } b, _ := json.Marshal(req) want := `{"cmd":2,"data":{"list":[{"type":"aGate","info":[{"sn":"AFCI_TEST_0713","index":[7]}]}]}}` if string(b) != want { t.Errorf("cmd=2 request:\n got %s\nwant %s", b, want) } b, _ = json.Marshal(NewExitRequest()) if string(b) != `{"cmd":4,"data":{}}` { t.Errorf("cmd=4 request: %s", b) } if _, err := NewRealtimeRequest("pe", "X", []int{4}); err == nil { t.Error("code=4 should be rejected for cmd=0") } if _, err := NewGetRecordRequest("mpt", "X", []int{1}); err == nil { t.Error("unknown device type should be rejected") } for in, want := range map[string]string{"pe": "pe", "AGATE": "aGate", "MPPT": "mppt", "apbox": "apbox", "dif": "dif"} { if got, err := NormalizeDeviceType(in); err != nil || got != want { t.Errorf("NormalizeDeviceType(%q) = %q, %v", in, got, err) } } req, _ = NewGetRecordRequest("aGate", "X", []int{7, 9, 3}) if b, _ = json.Marshal(req); !strings.Contains(string(b), `"index":[7,9,3]`) { t.Errorf("multi-record request: %s", b) } for _, bad := range [][]int{{}, {7, 7}, {0}, {256}} { if _, err := NewGetRecordRequest("aGate", "X", bad); err == nil { t.Errorf("indexes %v should be rejected", bad) } } } func TestSetVal(t *testing.T) { ok := []ParaItem{{1, ""}, {2, "021001100A"}, {4, "07EA0A100C0000"}, {5, "1194"}, {6, "010100015984"}, {7, "00"}} for _, p := range ok { if err := ValidateSetVal(p.Id, p.SetVal); err != nil { t.Errorf("id=%d %q: %v", p.Id, p.SetVal, err) } } bad := []ParaItem{{1, "00"}, {2, "0210"}, {5, "11"}, {6, "0101"}, {7, "02"}, {8, ""}, {5, "XYZ1"}} for _, p := range bad { if err := ValidateSetVal(p.Id, p.SetVal); err == nil { t.Errorf("id=%d %q should be invalid", p.Id, p.SetVal) } } if v := DecodeIdData(5, "1194").(map[string]any)["frequencyHz"]; v != uint16(4500) { t.Errorf("id=5 decode: %v", v) } c := DecodeIdData(6, "04030003FD34").(ConfigChannel) if c.Index != 4 || c.Mode != 0 || c.TypeName != "Float32" || c.Address != 0x3FD34 { t.Errorf("id=6 decode: %+v", c) } } func TestDescribeResultOne(t *testing.T) { cases := map[string]string{ "ok": "成功", "cancomerr:suberrcode=31": "子设备错误码 0x31:requestOutOfRange 请求超过范围", "cancomerr:index=e": "设备内部通信错误 0xE:记录传输阶段:接收超时", "something new": "something new", "cancomerr:suberrcode=zz": "子设备错误码 zz", "emserr:can p null": "未取到目标子设备", } for in, want := range cases { if got := DescribeResultOne(in); got != want { t.Errorf("DescribeResultOne(%q) = %q, want %q", in, got, want) } } } func TestParseUploadAckVersions(t *testing.T) { cases := []struct { name string in string want UploadAck }{ {"旧版本:第二个 result 为 id", `{"result":0,"filename":"B","result":5226}`, UploadAck{Result: 0, Id: 5226, Filename: "B"}}, {"旧版本:带空白", `{ "result" : 1 , "filename":"B", "result" : 7 }`, UploadAck{Result: 1, Id: 7, Filename: "B"}}, {"修复后版本", `{"result":0,"filename":"B","id":5226}`, UploadAck{Result: 0, Id: 5226, Filename: "B"}}, {"值中含 result 字样不受影响", `{"result":0,"filename":"result","result":9}`, UploadAck{Result: 0, Id: 9, Filename: "result"}}, {"只有一个 result", `{"result":2,"filename":"B"}`, UploadAck{Result: 2, Filename: "B"}}, } for _, c := range cases { got, err := ParseUploadAck([]byte(c.in)) if err != nil || got != c.want { t.Errorf("%s: got %+v %v, want %+v", c.name, got, err, c.want) } } if _, err := ParseUploadAck([]byte(`[1]`)); err == nil { t.Error("non-object should fail") } }