diff --git a/cmd/cli.go b/cmd/cli.go index edea3962..e58136f2 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -13,6 +13,7 @@ var commands struct { Format cli.Format `cmd:"" help:"disco ball Matter spec documents specified by the filename_pattern" group:"Spec Commands:"` Disco cli.Disco `cmd:"" help:"disco ball Matter spec documents specified by the filename_pattern" group:"Spec Commands:"` ZAP cli.ZAP `cmd:"" help:"transmute the Matter spec into ZAP templates, optionally filtered to the files specified by filename_pattern" group:"SDK Commands:"` + ZAPDiff cli.ZAPDiff `cmd:"" name:"zap-diff" help:"Compares two set of ZAP XMLs for any inconsistency." group:"SDK Commands:"` Conformance cli.Conformance `cmd:"" help:"test conformance values" group:"Spec Commands:"` Dump dump.Command `cmd:"" hidden:"" help:"dump the parse tree of Matter documents specified by filename_pattern"` DM cli.DataModel `cmd:"" help:"transmute the Matter spec into data model XML; optionally filtered to the files specified in filename_pattern" group:"SDK Commands:"` diff --git a/cmd/cli/zapdiff.go b/cmd/cli/zapdiff.go new file mode 100644 index 00000000..2c8936bd --- /dev/null +++ b/cmd/cli/zapdiff.go @@ -0,0 +1,125 @@ +package cli + +import ( + "encoding/csv" + "log/slog" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/project-chip/alchemy/zapdiff" +) + +type ZAPDiff struct { + XmlRoot1 string `help:"root of first set of ZAP XMLs" group:"SDK Commands:" required:"true"` + XmlRoot2 string `help:"root of second set of ZAP XMLs" group:"SDK Commands:" required:"true"` + Label1 string `default:"ZapXML-1" help:"label for first set of ZAP XMLs" group:"SDK Commands:"` + Label2 string `default:"ZapXML-2" help:"label for second set of ZAP XMLs" group:"SDK Commands:"` + Out string `default:"." help:"path to output mismatch.csv file" group:"SDK Commands:"` + MismatchLevel int `default:"3" help:"the minimum mismatch level to report (1-3)" group:"SDK Commands:"` +} + +func (z *ZAPDiff) Run(cc *Context) (err error) { + var mismatchPrintLevel zapdiff.XmlMismatchLevel + if z.MismatchLevel < 1 || z.MismatchLevel > 3 { + slog.Warn("invalid mismatch level. must be between 1 and 3.", "level", z.MismatchLevel) + mismatchPrintLevel = zapdiff.MismatchLevel3 // Default + } else { + mismatchPrintLevel = zapdiff.XmlMismatchLevel(z.MismatchLevel - 1) // Convert 1-3 to 0-2 + } + + ff1, err := listXMLFiles(z.XmlRoot1) + if err != nil { + slog.Error("error listing files", "dir", z.XmlRoot1, "error", err) + return err + } + + ff2, err := listXMLFiles(z.XmlRoot2) + if err != nil { + slog.Error("error listing files", "dir", z.XmlRoot2, "error", err) + return err + } + + mm := zapdiff.Pipeline(ff1, ff2, z.Label1, z.Label2) + + csvOutputPath := filepath.Join(z.Out, "mismatches.csv") + err = writeMismatchesToCSV(csvOutputPath, mm, mismatchPrintLevel) + if err != nil { + slog.Error("Failed to write CSV output", "error", err) + } + + return +} + +func listXMLFiles(p string) (paths []string, err error) { + var entries []os.DirEntry + entries, err = os.ReadDir(p) + if err != nil { + return + } + + for _, e := range entries { + if strings.HasSuffix(e.Name(), ".xml") { + paths = append(paths, filepath.Join(p, e.Name())) + } + } + + return +} + +func writeMismatchesToCSV(p string, mm []zapdiff.XmlMismatch, l zapdiff.XmlMismatchLevel) (err error) { + f, err := os.Create(p) + if err != nil { + slog.Error("failed to create file", "path", p, "error", err) + return err + } + defer f.Close() + + w := csv.NewWriter(f) + defer w.Flush() + + // Write header + header := []string{"Level", "Type", "File", "Element Xpath", "Details"} + if err = w.Write(header); err != nil { + slog.Error("failed to write CSV header", "error", err) + return + } + + sort.Slice(mm, func(i, j int) bool { + // Level (Descending), Path, Type, ElementID, Details + if mm[i].Level() != mm[j].Level() { + return mm[i].Level() > mm[j].Level() + } + if mm[i].Path != mm[j].Path { + return mm[i].Path < mm[j].Path + } + if mm[i].Type != mm[j].Type { + return mm[i].Type.String() < mm[j].Type.String() + } + if mm[i].ElementID != mm[j].ElementID { + return mm[i].ElementID < mm[j].ElementID + } + return mm[i].Details < mm[j].Details + }) + + // Write mismatches + for _, m := range mm { + if m.Level() >= l { + row := []string{ + m.Level().String(), + m.Type.String(), + m.Path, + m.ElementID, + m.Details, + } + if err = w.Write(row); err != nil { + slog.Error("Warning: failed to write row to CSV", "err", err) + return + } + } + } + + slog.Info("Successfully wrote mismatches to CSV", "dir", p) + return +} diff --git a/zapdiff/check.go b/zapdiff/check.go new file mode 100644 index 00000000..5b25a4ee --- /dev/null +++ b/zapdiff/check.go @@ -0,0 +1,108 @@ +package zapdiff + +import ( + "fmt" + + "github.com/beevik/etree" +) + +func checkMismatches(ep elementPair, baseName string, n1, n2 string) (mm []XmlMismatch) { + e1Children := make(map[string]*etree.Element) + e2Children := make(map[string]*etree.Element) + mm = make([]XmlMismatch, 0) + + for _, c1 := range ep.e1.ChildElements() { + id := getElementID(c1) + e1Children[id] = c1 + } + + for _, c2 := range ep.e2.ChildElements() { + id := getElementID(c2) + e2Children[id] = c2 + } + + for id, e1 := range e1Children { + if _, ok := e2Children[id]; !ok { + m := XmlMismatch{ + Path: baseName, + Type: getMismatchMissingType(e1), + Details: fmt.Sprintf("Only found in %s", n1), + ElementID: id, + } + mm = append(mm, m) + } + } + + for id, e2 := range e2Children { + if _, ok := e1Children[id]; !ok { + m := XmlMismatch{ + Path: baseName, + Type: getMismatchMissingType(e2), + Details: fmt.Sprintf("Only found in %s", n2), + ElementID: id, + } + mm = append(mm, m) + } + } + + // Recurse into common tags + for id, e1 := range e1Children { + if e2, ok := e2Children[id]; ok { + // Check attributes + attrMM := checkAttributes(elementPair{e1: e1, e2: e2}, id, baseName, n1, n2) + mm = append(mm, attrMM...) + + // Recurse + subMM := checkMismatches(elementPair{e1: e1, e2: e2}, baseName, n1, n2) + mm = append(mm, subMM...) + } + } + + return +} + +func checkAttributes(ep elementPair, id string, baseName string, n1, n2 string) (mm []XmlMismatch) { + mm = make([]XmlMismatch, 0) + e1Attrs := make(map[string]string) + e2Attrs := make(map[string]string) + + for _, a := range ep.e1.Attr { + e1Attrs[a.Key] = a.Value + } + for _, a := range ep.e2.Attr { + e2Attrs[a.Key] = a.Value + } + + for k, v1 := range e1Attrs { + if v2, ok := e2Attrs[k]; !ok { + m := XmlMismatch{ + Path: baseName, + Type: getMismatchMissingAttrType(ep.e1), + Details: fmt.Sprintf("Attribute [%s] only found in %s", k, n1), + ElementID: id, + } + mm = append(mm, m) + } else if v1 != v2 { + m := XmlMismatch{ + Path: baseName, + Type: getMismatchAttrValueType(ep.e1), + Details: fmt.Sprintf("Attribute [%s] has different values: '%s' in %s, '%s' in %s", k, v1, n1, v2, n2), + ElementID: id, + } + mm = append(mm, m) + } + } + + for k := range e2Attrs { + if _, ok := e1Attrs[k]; !ok { + m := XmlMismatch{ + Path: baseName, + Type: getMismatchMissingAttrType(ep.e2), + Details: fmt.Sprintf("Attribute [%s] only found in %s", k, n2), + ElementID: id, + } + mm = append(mm, m) + } + } + return +} diff --git a/zapdiff/element_id.go b/zapdiff/element_id.go new file mode 100644 index 00000000..c7de588d --- /dev/null +++ b/zapdiff/element_id.go @@ -0,0 +1,72 @@ +package zapdiff + +import ( + "fmt" + + "github.com/beevik/etree" +) + +func parentAndSelfAttr(e *etree.Element, attr string) string { + parentID := getElementID(e.Parent()) + return fmt.Sprintf("%s/%s[@%s='%s']", parentID, e.Tag, attr, e.SelectAttrValue(attr, "")) +} + +func parentAndSelfText(e *etree.Element) string { + parentID := getElementID(e.Parent()) + return fmt.Sprintf("%s[%s='%s']/%s", parentID, e.Tag, e.Text(), e.Tag) +} + +func getElementID(e *etree.Element) string { + if e == nil { + return "" + } + p := e.GetPath() + + switch p { + case "/configurator": + return "configurator" + case "/configurator/global/attribute", + "/configurator/enum", + "/configurator/enum/item", + "/configurator/struct", + "/configurator/struct/item", + "/configurator/bitmap", + "/configurator/bitmap/field", + "/configurator/cluster/command", + "/configurator/cluster/command/arg", + "/configurator/cluster/attribute", + "/configurator/cluster/event", + "/configurator/cluster/event/field", + "/configurator/cluster/features/feature": + return parentAndSelfAttr(e, "name") + case "/configurator/enum/cluster", + "/configurator/struct/cluster": + return parentAndSelfAttr(e, "code") + case "/configurator/cluster": + parentID := getElementID(e.Parent()) + code := e.SelectAttrValue("code", "") + if code != "" { + return fmt.Sprintf("%s/%s[@code='%s']", parentID, e.Tag, code) + } + nameEl := e.SelectElement("name") + if nameEl != nil { + nameText := nameEl.Text() + return fmt.Sprintf("%s/%s[name='%s']", parentID, e.Tag, nameText) + } else { + return getElementXPathSegment(e) + } + case "/configurator/cluster/name", + "/configurator/cluster/domain", + "/configurator/cluster/description", + "/configurator/cluster/code", + "/configurator/cluster/define", + "/configurator/cluster/client", + "/configurator/cluster/server": + return parentAndSelfText(e) + + default: + parentID := getElementID(e.Parent()) + selfSegment := getElementXPathSegment(e) + return fmt.Sprintf("%s/%s", parentID, selfSegment) + } +} diff --git a/zapdiff/file.go b/zapdiff/file.go new file mode 100644 index 00000000..a1498fd8 --- /dev/null +++ b/zapdiff/file.go @@ -0,0 +1,92 @@ +package zapdiff + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strings" +) + +const alchemyComment = "XML generated by Alchemy; DO NOT EDIT." + +func isAlchemyFile(p string) bool { + f, err := os.Open(p) + if err != nil { + return false + } + defer f.Close() + + // Read first 30 lines to find the comment + s := bufio.NewScanner(f) + for i := 0; i < 30 && s.Scan(); i++ { + if strings.Contains(s.Text(), alchemyComment) { + return true + } + } + return false +} + +func excludeNonAlchemyFiles(ff []string) (out []string) { + out = make([]string, 0) + for _, p := range ff { + if isAlchemyFile(p) { + out = append(out, p) + } + } + return out +} + +func getFilePairs(ff1, ff2 []string) (common []filePair) { + map2 := make(map[string]string, len(ff2)) + for _, p2 := range ff2 { + map2[filepath.Base(p2)] = p2 + } + + common = make([]filePair, 0) + for _, p1 := range ff1 { + base1 := filepath.Base(p1) + if p2, ok := map2[base1]; ok { + common = append(common, filePair{p1: p1, p2: p2}) + } + } + return +} + +func fileListDiff(ff1, ff2 []string, n1, n2 string) (mm []XmlMismatch) { + map1 := make(map[string]string, len(ff1)) + for _, p := range ff1 { + map1[filepath.Base(p)] = p + } + + map2 := make(map[string]string, len(ff2)) + for _, p := range ff2 { + map2[filepath.Base(p)] = p + } + + for b1 := range map1 { + if _, ok := map2[b1]; !ok { + m := XmlMismatch{ + Path: b1, + Type: XmlMismatchNewFile, + Details: fmt.Sprintf("Only found in %s, or the file is not alchemy-generated.", n1), + ElementID: b1, + } + mm = append(mm, m) + } + } + + for b2 := range map2 { + if _, ok := map1[b2]; !ok { + m := XmlMismatch{ + Path: b2, + Type: XmlMismatchNewFile, + Details: fmt.Sprintf("Only found in %s, or the file is not alchemy-generated.", n2), + ElementID: b2, + } + mm = append(mm, m) + } + } + + return +} diff --git a/zapdiff/pipeline.go b/zapdiff/pipeline.go new file mode 100644 index 00000000..1c86ff79 --- /dev/null +++ b/zapdiff/pipeline.go @@ -0,0 +1,63 @@ +package zapdiff + +import ( + "log/slog" + "path/filepath" + + "github.com/beevik/etree" +) + +type filePair struct { + p1 string + p2 string +} + +type elementPair struct { + e1 *etree.Element + e2 *etree.Element +} + +func Pipeline(ff1, ff2 []string, n1, n2 string) (mm []XmlMismatch) { + mm = make([]XmlMismatch, 0) + + // Filter manual files + f1 := excludeNonAlchemyFiles(ff1) + f2 := excludeNonAlchemyFiles(ff2) + ff := getFilePairs(f1, f2) + + mm = append(mm, fileListDiff(f1, f2, n1, n2)...) + + for _, f := range ff { + baseName := filepath.Base(f.p1) + + d1 := etree.NewDocument() + d2 := etree.NewDocument() + + err := d1.ReadFromFile(f.p1) + if err != nil { + slog.Warn("Failed to parse", "file", f.p1, "error", err) + continue + } + err = d2.ReadFromFile(f.p2) + if err != nil { + slog.Warn("Failed to parse", "file", f.p2, "error", err) + continue + } + + r1 := d1.Root() + r2 := d2.Root() + + if r1 == nil { + slog.Warn("File has no root element", "file", baseName, "clone", n1) + continue + } + if r2 == nil { + slog.Warn("File has no root element", "file", baseName, "clone", n2) + continue + } + + emm := checkMismatches(elementPair{e1: r1, e2: r2}, baseName, n1, n2) + mm = append(mm, emm...) + } + return +} diff --git a/zapdiff/tag_mismatch_map.go b/zapdiff/tag_mismatch_map.go new file mode 100644 index 00000000..1a7c4912 --- /dev/null +++ b/zapdiff/tag_mismatch_map.go @@ -0,0 +1,92 @@ +package zapdiff + +import "github.com/beevik/etree" + +func getMismatchMissingType(e *etree.Element) XmlMismatchType { + p := e.GetPath() + switch p { + case "/configurator/enum": + return XmlMismatchMissingEnum + case "/configurator/enum/item": + return XmlMismatchMissingEnumItem + case "/configurator/struct": + return XmlMismatchMissingStruct + case "/configurator/struct/item": + return XmlMismatchMissingStructItem + case "/configurator/bitmap": + return XmlMismatchMissingBitmap + case "/configurator/bitmap/field": + return XmlMismatchMissingBitmapField + case "/configurator/cluster": + return XmlMismatchMissingCluster + case "/configurator/cluster/command": + return XmlMismatchMissingClusterCommand + case "/configurator/cluster/attribute": + return XmlMismatchMissingClusterAttribute + case "/configurator/cluster/event": + return XmlMismatchMissingClusterEvent + + case "/configurator/cluster/name", + "/configurator/cluster/domain", + "/configurator/cluster/description", + "/configurator/cluster/code", + "/configurator/cluster/define", + "/configurator/cluster/client", + "/configurator/cluster/server": + return XmlMismatchClusterDetails + + case "/configurator/cluster/features/feature": + return XmlMismatchMissingClusterFeature + + default: + return XmlMismatchMissingTag + } +} + +func getMismatchMissingAttrType(e *etree.Element) XmlMismatchType { + p := e.GetPath() + switch p { + case "/configurator/struct/item": + return XmlMismatchStructItemMissingAttr + case "/configurator/enum/item": + return XmlMismatchEnumItemMissingAttr + case "/configurator/bitmap": + return XmlMismatchBitmapMissingAttr + case "/configurator/bitmap/field": + return XmlMismatchBitmapFieldMissingAttr + case "/configurator/cluster": + return XmlMismatchClusterMissingAttr + case "/configurator/cluster/command": + return XmlMismatchClusterCommandMissingAttr + case "/configurator/cluster/attribute": + return XmlMismatchClusterAttributeMissingAttr + case "/configurator/cluster/event": + return XmlMismatchClusterEventMissingAttr + default: + return XmlMismatchMissingAttr + } +} + +func getMismatchAttrValueType(e *etree.Element) XmlMismatchType { + p := e.GetPath() + switch p { + case "/configurator/struct/item": + return XmlMismatchStructItemAttrValue + case "/configurator/enum/item": + return XmlMismatchEnumItemAttrValue + case "/configurator/bitmap": + return XmlMismatchBitmapAttrValue + case "/configurator/bitmap/field": + return XmlMismatchBitmapFieldAttrValue + case "/configurator/cluster": + return XmlMismatchClusterAttrValue + case "/configurator/cluster/command": + return XmlMismatchClusterCommandAttrValue + case "/configurator/cluster/attribute": + return XmlMismatchClusterAttributeAttrValue + case "/configurator/cluster/event": + return XmlMismatchClusterEventAttrValue + default: + return XmlMismatchAttrValue + } +} diff --git a/zapdiff/xml_mismatch.go b/zapdiff/xml_mismatch.go new file mode 100644 index 00000000..f6f648f0 --- /dev/null +++ b/zapdiff/xml_mismatch.go @@ -0,0 +1,268 @@ +package zapdiff + +import "fmt" + +type XmlMismatchLevel uint8 + +const ( + MismatchLevel1 XmlMismatchLevel = iota + MismatchLevel2 + MismatchLevel3 +) + +func (l XmlMismatchLevel) String() string { + switch l { + case MismatchLevel1: + return "L1" + case MismatchLevel2: + return "L2" + case MismatchLevel3: + return "L3" + + default: + return "UNKNOWN" + } +} + +type XmlMismatchType uint8 + +const ( + XmlMismatchNone XmlMismatchType = iota + + // File level + XmlMismatchNewFile + + // Generic Tag/Attr Mismatches + XmlMismatchMissingTag + XmlMismatchMissingAttr + XmlMismatchAttrValue + + // Enums + XmlMismatchMissingEnum + XmlMismatchMissingEnumItem + XmlMismatchEnumItemMissingAttr + XmlMismatchEnumItemAttrValue + + // Structs + XmlMismatchMissingStruct + XmlMismatchMissingStructItem + XmlMismatchStructItemMissingAttr + XmlMismatchStructItemAttrValue + + // Bitmaps + XmlMismatchMissingBitmap + XmlMismatchMissingBitmapField + XmlMismatchBitmapMissingAttr + XmlMismatchBitmapAttrValue + XmlMismatchBitmapFieldMissingAttr + XmlMismatchBitmapFieldAttrValue + + // Clusters (Top Level) + XmlMismatchMissingCluster + XmlMismatchClusterMissingAttr + XmlMismatchClusterAttrValue + + // Clusters + XmlMismatchMissingClusterCommand + XmlMismatchClusterCommandMissingAttr + XmlMismatchClusterCommandAttrValue + XmlMismatchMissingClusterAttribute + XmlMismatchClusterAttributeMissingAttr + XmlMismatchClusterAttributeAttrValue + XmlMismatchMissingClusterEvent + XmlMismatchClusterEventMissingAttr + XmlMismatchClusterEventAttrValue + XmlMismatchMissingClusterFeature + + XmlMismatchClusterDetails +) + +func (t XmlMismatchType) String() string { + switch t { + case XmlMismatchNone: + return "None" + + // File + case XmlMismatchNewFile: + return "FileNotFound" + + // Generic + case XmlMismatchMissingTag: + return "MissingTag" + case XmlMismatchMissingAttr: + return "MissingAttr" + case XmlMismatchAttrValue: + return "AttrValue" + + // Enums + case XmlMismatchMissingEnum: + return "MissingEnum" + case XmlMismatchMissingEnumItem: + return "MissingEnumItem" + case XmlMismatchEnumItemMissingAttr: + return "EnumItemMissingAttr" + case XmlMismatchEnumItemAttrValue: + return "EnumItemAttrValue" + + // Structs + case XmlMismatchMissingStruct: + return "MissingStruct" + case XmlMismatchMissingStructItem: + return "MissingStructItem" + case XmlMismatchStructItemMissingAttr: + return "StructItemMissingAttr" + case XmlMismatchStructItemAttrValue: + return "StructItemAttrValue" + + // Bitmaps + case XmlMismatchMissingBitmap: + return "MissingBitmap" + case XmlMismatchMissingBitmapField: + return "MissingBitmapField" + case XmlMismatchBitmapMissingAttr: + return "BitmapMissingAttr" + case XmlMismatchBitmapAttrValue: + return "BitmapAttrValue" + case XmlMismatchBitmapFieldMissingAttr: + return "BitmapFieldMissingAttr" + case XmlMismatchBitmapFieldAttrValue: + return "BitmapFieldAttrValue" + + // Clusters (Top Level) + case XmlMismatchMissingCluster: + return "MissingCluster" + case XmlMismatchClusterMissingAttr: + return "ClusterMissingAttr" + case XmlMismatchClusterAttrValue: + return "ClusterAttrValue" + + // Clusters + case XmlMismatchMissingClusterCommand: + return "MissingClusterCommand" + case XmlMismatchClusterCommandMissingAttr: + return "ClusterCommandMissingAttr" + case XmlMismatchClusterCommandAttrValue: + return "ClusterCommandAttrValue" + case XmlMismatchMissingClusterAttribute: + return "MissingClusterAttribute" + case XmlMismatchClusterAttributeMissingAttr: + return "ClusterAttributeMissingAttr" + case XmlMismatchClusterAttributeAttrValue: + return "ClusterAttributeAttrValue" + case XmlMismatchMissingClusterEvent: + return "MissingClusterEvent" + case XmlMismatchClusterEventMissingAttr: + return "ClusterEventMissingAttr" + case XmlMismatchClusterEventAttrValue: + return "ClusterEventAttrValue" + case XmlMismatchMissingClusterFeature: + return "MissingClusterFeature" + + case XmlMismatchClusterDetails: + return "ClusterDetails" + + default: + return "Unknown Mismatch" + } +} + +func (t XmlMismatchType) Level() XmlMismatchLevel { + switch t { + // File + case XmlMismatchNewFile: + return MismatchLevel1 + + // Generic + case XmlMismatchMissingTag: + return MismatchLevel2 + case XmlMismatchMissingAttr: + return MismatchLevel1 + case XmlMismatchAttrValue: + return MismatchLevel2 + + // Enums + case XmlMismatchMissingEnum: + return MismatchLevel3 + case XmlMismatchMissingEnumItem: + return MismatchLevel3 + case XmlMismatchEnumItemMissingAttr: + return MismatchLevel1 + case XmlMismatchEnumItemAttrValue: + return MismatchLevel3 + + // Structs + case XmlMismatchMissingStruct: + return MismatchLevel3 + case XmlMismatchMissingStructItem: + return MismatchLevel3 + case XmlMismatchStructItemMissingAttr: + return MismatchLevel1 + case XmlMismatchStructItemAttrValue: + return MismatchLevel3 + + // Bitmaps + case XmlMismatchMissingBitmap: + return MismatchLevel3 + case XmlMismatchMissingBitmapField: + return MismatchLevel3 + case XmlMismatchBitmapMissingAttr: + return MismatchLevel1 + case XmlMismatchBitmapAttrValue: + return MismatchLevel3 + case XmlMismatchBitmapFieldMissingAttr: + return MismatchLevel1 + case XmlMismatchBitmapFieldAttrValue: + return MismatchLevel3 + + // Clusters (Top Level) + case XmlMismatchMissingCluster: + return MismatchLevel3 + case XmlMismatchClusterMissingAttr: + return MismatchLevel1 + case XmlMismatchClusterAttrValue: + return MismatchLevel3 + + // Clusters + case XmlMismatchMissingClusterCommand: + return MismatchLevel3 + case XmlMismatchClusterCommandMissingAttr: + return MismatchLevel1 + case XmlMismatchClusterCommandAttrValue: + return MismatchLevel3 + case XmlMismatchMissingClusterAttribute: + return MismatchLevel3 + case XmlMismatchClusterAttributeMissingAttr: + return MismatchLevel1 + case XmlMismatchClusterAttributeAttrValue: + return MismatchLevel3 + case XmlMismatchMissingClusterEvent: + return MismatchLevel3 + case XmlMismatchClusterEventMissingAttr: + return MismatchLevel1 + case XmlMismatchClusterEventAttrValue: + return MismatchLevel3 + case XmlMismatchMissingClusterFeature: + return MismatchLevel3 + + case XmlMismatchClusterDetails: + return MismatchLevel3 + + default: + return MismatchLevel1 + } +} + +type XmlMismatch struct { + Path string + Details string + Type XmlMismatchType + ElementID string +} + +func (m XmlMismatch) Level() XmlMismatchLevel { + return m.Type.Level() +} + +func (m *XmlMismatch) Error() string { + return fmt.Sprintf("[%s] %s - in %s: %s", m.Level().String(), m.Type.String(), m.Path, m.Details) +} diff --git a/zapdiff/xpath.go b/zapdiff/xpath.go new file mode 100644 index 00000000..161e54ad --- /dev/null +++ b/zapdiff/xpath.go @@ -0,0 +1,32 @@ +package zapdiff + +import ( + "fmt" + + "github.com/beevik/etree" +) + +func getElementXPathSegment(e *etree.Element) (s string) { + p := e.Parent() + if p == nil { + return e.Tag + } + + idx := 0 + cnt := 0 + for _, sib := range p.ChildElements() { + if sib.Tag == e.Tag { + cnt++ + if sib == e { + idx = cnt + break + } + } + } + + s = e.Tag + if cnt > 1 { + s += fmt.Sprintf("[%d]", idx) + } + return s +}