From 7406c6589b463e6304060d622461ce68d8eed5bf Mon Sep 17 00:00:00 2001 From: Arya Hassanli Date: Thu, 23 Oct 2025 21:53:34 +0000 Subject: [PATCH 1/6] Introduce zapdiff --- cmd/action/action.go | 1 + cmd/action/zap_diff.go | 12 ++ cmd/cli.go | 1 + cmd/cli/zapdiff.go | 119 ++++++++++++++++ zapdiff/check.go | 108 ++++++++++++++ zapdiff/element_id.go | 69 +++++++++ zapdiff/file.go | 98 +++++++++++++ zapdiff/pipeline.go | 63 +++++++++ zapdiff/tag_mismatch_map.go | 92 ++++++++++++ zapdiff/xml_mismatch.go | 274 ++++++++++++++++++++++++++++++++++++ zapdiff/xpath.go | 32 +++++ 11 files changed, 869 insertions(+) create mode 100644 cmd/action/zap_diff.go create mode 100644 cmd/cli/zapdiff.go create mode 100644 zapdiff/check.go create mode 100644 zapdiff/element_id.go create mode 100644 zapdiff/file.go create mode 100644 zapdiff/pipeline.go create mode 100644 zapdiff/tag_mismatch_map.go create mode 100644 zapdiff/xml_mismatch.go create mode 100644 zapdiff/xpath.go diff --git a/cmd/action/action.go b/cmd/action/action.go index ba12446d..7af437ed 100644 --- a/cmd/action/action.go +++ b/cmd/action/action.go @@ -4,5 +4,6 @@ type Action struct { Comment Comment `cmd:"" help:"GitHub action for Matter spec documents"` Disco Disco `cmd:"" default:"" help:"GitHub action for Matter spec documents"` ZAP ZAP `cmd:"" help:"GitHub action for Matter SDK ZAP XML"` + ZAPDiff ZAPDiff `cmd:"zap-diff" help:"GitHub action for Matter SDK ZAP Diff XML"` MergeGuard MergeGuard `cmd:"" help:"GitHub action to prevent Provisionality and Parse errors to be merged."` } diff --git a/cmd/action/zap_diff.go b/cmd/action/zap_diff.go new file mode 100644 index 00000000..2953cbf7 --- /dev/null +++ b/cmd/action/zap_diff.go @@ -0,0 +1,12 @@ +package action + +import ( + "github.com/project-chip/alchemy/cmd/cli" +) + +type ZAPDiff struct { +} + +func (z *ZAPDiff) Run(cc *cli.Context) (err error) { + return +} diff --git a/cmd/cli.go b/cmd/cli.go index c78cc101..d0dfe0f3 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 incosistency." 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..391d7066 --- /dev/null +++ b/cmd/cli/zapdiff.go @@ -0,0 +1,119 @@ +package cli + +import ( + "encoding/csv" + "log/slog" + "os" + "path/filepath" + "strings" + + "github.com/project-chip/alchemy/matter/spec" + "github.com/project-chip/alchemy/sdk" + "github.com/project-chip/alchemy/zapdiff" +) + +type ZAPDiff struct { + spec.FilterOptions `embed:""` + SdkRoot1 string `default:"connectedhomeip" help:"the first clone of project-chip/connectedhomeip" group:"SDK Commands:"` + SdkRoot2 string `default:"connectedhomeip" help:"the second clone of project-chip/connectedhomeip" 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-5)" group:"SDK Commands:"` +} + +func (z *ZAPDiff) Run(cc *Context) (err error) { + var mismatchPrintLevel zapdiff.XmlMismatchLevel + if z.MismatchLevel < 1 || z.MismatchLevel > 5 { + slog.Warn("invalid mismatch level. must be between 1 and 5.", "level", z.MismatchLevel) + mismatchPrintLevel = zapdiff.MismatchLevel3 // Default + } else { + mismatchPrintLevel = zapdiff.XmlMismatchLevel(z.MismatchLevel - 1) // Convert 1-5 to 0-4 + } + + err = sdk.CheckAlchemyVersion(z.SdkRoot1) + if err != nil { + return + } + + err = sdk.CheckAlchemyVersion(z.SdkRoot2) + if err != nil { + return + } + + p1 := filepath.Join(z.SdkRoot1, "src", "app", "zap-templates", "zcl", "data-model", "chip") + ff1, err := listXMLFiles(p1) + if err != nil { + slog.Error("error listing files", "dir", p1, "error", err) + return + } + + p2 := filepath.Join(z.SdkRoot2, "src", "app", "zap-templates", "zcl", "data-model", "chip") + ff2, err := listXMLFiles(p2) + if err != nil { + slog.Error("error listing files", "dir", p2, "error", err) + return + } + + mm := zapdiff.Pipeline(ff1, ff2, "sdk-1", "skd-2") + + 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 + } + 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 + } + + // 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.Warn("Warning: failed to write row to CSV", "err", err) + } + } + } + + slog.Info("Successfully wrote mismatches to CSV", "dir", p) + return nil +} 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..c53cc672 --- /dev/null +++ b/zapdiff/element_id.go @@ -0,0 +1,69 @@ +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 { + 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..8343487a --- /dev/null +++ b/zapdiff/file.go @@ -0,0 +1,98 @@ +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) { + common = make([]filePair, 0) + for _, p1 := range ff1 { + base1 := filepath.Base(p1) + for _, p2 := range ff2 { + if filepath.Base(p2) == base1 { + common = append(common, filePair{p1: p1, p2: p2}) + break + } + } + } + return +} + +func fileListDiff(ff1, ff2 []string, n1, n2 string) (mm []XmlMismatch) { + mm = make([]XmlMismatch, 0) + + for _, p1 := range ff1 { + b1 := filepath.Base(p1) + found := false + for _, p2 := range ff2 { + if filepath.Base(p2) == b1 { + found = true + break + } + } + if !found { + 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 _, p2 := range ff2 { + b2 := filepath.Base(p2) + found := false + for _, p1 := range ff1 { + if filepath.Base(p1) == b2 { + found = true + break + } + } + if !found { + 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..18da8265 --- /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 %s: %v\n", f.p1, err) + continue + } + err = d2.ReadFromFile(f.p2) + if err != nil { + slog.Warn("Failed to parse %s: %v\n", f.p2, err) + continue + } + + r1 := d1.Root() + r2 := d2.Root() + + if r1 == nil { + slog.Warn("File %s (%s) has no root element\n", baseName, n1) + continue + } + if r2 == nil { + slog.Warn("File %s (%s) has no root element\n", baseName, 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..06063fea --- /dev/null +++ b/zapdiff/xml_mismatch.go @@ -0,0 +1,274 @@ +package zapdiff + +import "fmt" + +type XmlMismatchLevel uint8 + +const ( + MismatchLevel1 XmlMismatchLevel = iota + MismatchLevel2 + MismatchLevel3 + MismatchLevel4 + MismatchLevel5 +) + +func (l XmlMismatchLevel) String() string { + switch l { + case MismatchLevel1: + return "L1" + case MismatchLevel2: + return "L2" + case MismatchLevel3: + return "L3" + case MismatchLevel4: + return "L4" + case MismatchLevel5: + return "L5" + + 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 MismatchLevel4 + case XmlMismatchMissingEnumItem: + return MismatchLevel4 + case XmlMismatchEnumItemMissingAttr: + return MismatchLevel1 + case XmlMismatchEnumItemAttrValue: + return MismatchLevel4 + + // Structs + case XmlMismatchMissingStruct: + return MismatchLevel4 + case XmlMismatchMissingStructItem: + return MismatchLevel4 + case XmlMismatchStructItemMissingAttr: + return MismatchLevel1 + case XmlMismatchStructItemAttrValue: + return MismatchLevel4 + + // Bitmaps + case XmlMismatchMissingBitmap: + return MismatchLevel4 + case XmlMismatchMissingBitmapField: + return MismatchLevel4 + case XmlMismatchBitmapMissingAttr: + return MismatchLevel1 + case XmlMismatchBitmapAttrValue: + return MismatchLevel4 + case XmlMismatchBitmapFieldMissingAttr: + return MismatchLevel1 + case XmlMismatchBitmapFieldAttrValue: + return MismatchLevel4 + + // Clusters (Top Level) + case XmlMismatchMissingCluster: + return MismatchLevel4 + case XmlMismatchClusterMissingAttr: + return MismatchLevel1 + case XmlMismatchClusterAttrValue: + return MismatchLevel4 + + // Clusters + case XmlMismatchMissingClusterCommand: + return MismatchLevel4 + case XmlMismatchClusterCommandMissingAttr: + return MismatchLevel1 + case XmlMismatchClusterCommandAttrValue: + return MismatchLevel4 + case XmlMismatchMissingClusterAttribute: + return MismatchLevel4 + case XmlMismatchClusterAttributeMissingAttr: + return MismatchLevel1 + case XmlMismatchClusterAttributeAttrValue: + return MismatchLevel4 + case XmlMismatchMissingClusterEvent: + return MismatchLevel4 + case XmlMismatchClusterEventMissingAttr: + return MismatchLevel1 + case XmlMismatchClusterEventAttrValue: + return MismatchLevel4 + case XmlMismatchMissingClusterFeature: + return MismatchLevel4 + + case XmlMismatchClusterDetails: + return MismatchLevel4 + + 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 +} From 2ebb93daa9c3a287cb9e106477cc931555c808c1 Mon Sep 17 00:00:00 2001 From: Arya Hassanli Date: Tue, 28 Oct 2025 18:50:23 +0000 Subject: [PATCH 2/6] Sort mismatches --- cmd/cli/zapdiff.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/cmd/cli/zapdiff.go b/cmd/cli/zapdiff.go index 391d7066..bb6fd0d5 100644 --- a/cmd/cli/zapdiff.go +++ b/cmd/cli/zapdiff.go @@ -5,6 +5,7 @@ import ( "log/slog" "os" "path/filepath" + "sort" "strings" "github.com/project-chip/alchemy/matter/spec" @@ -98,6 +99,23 @@ func writeMismatchesToCSV(p string, mm []zapdiff.XmlMismatch, l zapdiff.XmlMisma 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.String() != mm[j].Type.String() { + 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 { From 2db7edd9711eb8ca475a24f0db7d700b653f801f Mon Sep 17 00:00:00 2001 From: Arya Hassanli Date: Tue, 28 Oct 2025 19:36:41 +0000 Subject: [PATCH 3/6] Remove action --- cmd/action/action.go | 1 - cmd/action/zap_diff.go | 12 ------------ 2 files changed, 13 deletions(-) delete mode 100644 cmd/action/zap_diff.go diff --git a/cmd/action/action.go b/cmd/action/action.go index 7af437ed..ba12446d 100644 --- a/cmd/action/action.go +++ b/cmd/action/action.go @@ -4,6 +4,5 @@ type Action struct { Comment Comment `cmd:"" help:"GitHub action for Matter spec documents"` Disco Disco `cmd:"" default:"" help:"GitHub action for Matter spec documents"` ZAP ZAP `cmd:"" help:"GitHub action for Matter SDK ZAP XML"` - ZAPDiff ZAPDiff `cmd:"zap-diff" help:"GitHub action for Matter SDK ZAP Diff XML"` MergeGuard MergeGuard `cmd:"" help:"GitHub action to prevent Provisionality and Parse errors to be merged."` } diff --git a/cmd/action/zap_diff.go b/cmd/action/zap_diff.go deleted file mode 100644 index 2953cbf7..00000000 --- a/cmd/action/zap_diff.go +++ /dev/null @@ -1,12 +0,0 @@ -package action - -import ( - "github.com/project-chip/alchemy/cmd/cli" -) - -type ZAPDiff struct { -} - -func (z *ZAPDiff) Run(cc *cli.Context) (err error) { - return -} From 1df120cd575d3a5be88e4797b4ec2638257fb935 Mon Sep 17 00:00:00 2001 From: Arya Hassanli Date: Tue, 28 Oct 2025 20:02:15 +0000 Subject: [PATCH 4/6] Fix findings --- cmd/cli.go | 2 +- cmd/cli/zapdiff.go | 17 ++++++++-------- zapdiff/element_id.go | 3 +++ zapdiff/file.go | 46 +++++++++++++++++++------------------------ zapdiff/pipeline.go | 8 ++++---- 5 files changed, 37 insertions(+), 39 deletions(-) diff --git a/cmd/cli.go b/cmd/cli.go index d0dfe0f3..c00c2bd7 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -13,7 +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 incosistency." 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 index bb6fd0d5..bd8e9629 100644 --- a/cmd/cli/zapdiff.go +++ b/cmd/cli/zapdiff.go @@ -44,17 +44,17 @@ func (z *ZAPDiff) Run(cc *Context) (err error) { ff1, err := listXMLFiles(p1) if err != nil { slog.Error("error listing files", "dir", p1, "error", err) - return + return err } p2 := filepath.Join(z.SdkRoot2, "src", "app", "zap-templates", "zcl", "data-model", "chip") ff2, err := listXMLFiles(p2) if err != nil { slog.Error("error listing files", "dir", p2, "error", err) - return + return err } - mm := zapdiff.Pipeline(ff1, ff2, "sdk-1", "skd-2") + mm := zapdiff.Pipeline(ff1, ff2, "sdk-1", "sdk-2") csvOutputPath := filepath.Join(z.Out, "mismatches.csv") err = writeMismatchesToCSV(csvOutputPath, mm, mismatchPrintLevel) @@ -85,7 +85,7 @@ func writeMismatchesToCSV(p string, mm []zapdiff.XmlMismatch, l zapdiff.XmlMisma f, err := os.Create(p) if err != nil { slog.Error("failed to create file", "path", p, "error", err) - return + return err } defer f.Close() @@ -107,7 +107,7 @@ func writeMismatchesToCSV(p string, mm []zapdiff.XmlMismatch, l zapdiff.XmlMisma if mm[i].Path != mm[j].Path { return mm[i].Path < mm[j].Path } - if mm[i].Type.String() != mm[j].Type.String() { + if mm[i].Type != mm[j].Type { return mm[i].Type.String() < mm[j].Type.String() } if mm[i].ElementID != mm[j].ElementID { @@ -126,12 +126,13 @@ func writeMismatchesToCSV(p string, mm []zapdiff.XmlMismatch, l zapdiff.XmlMisma m.ElementID, m.Details, } - if err := w.Write(row); err != nil { - slog.Warn("Warning: failed to write row to CSV", "err", err) + 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 nil + return } diff --git a/zapdiff/element_id.go b/zapdiff/element_id.go index c53cc672..c7de588d 100644 --- a/zapdiff/element_id.go +++ b/zapdiff/element_id.go @@ -17,6 +17,9 @@ func parentAndSelfText(e *etree.Element) string { } func getElementID(e *etree.Element) string { + if e == nil { + return "" + } p := e.GetPath() switch p { diff --git a/zapdiff/file.go b/zapdiff/file.go index 8343487a..a1498fd8 100644 --- a/zapdiff/file.go +++ b/zapdiff/file.go @@ -38,32 +38,34 @@ func excludeNonAlchemyFiles(ff []string) (out []string) { } 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) - for _, p2 := range ff2 { - if filepath.Base(p2) == base1 { - common = append(common, filePair{p1: p1, p2: p2}) - break - } + 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) { - mm = make([]XmlMismatch, 0) + map1 := make(map[string]string, len(ff1)) + for _, p := range ff1 { + map1[filepath.Base(p)] = p + } - for _, p1 := range ff1 { - b1 := filepath.Base(p1) - found := false - for _, p2 := range ff2 { - if filepath.Base(p2) == b1 { - found = true - break - } - } - if !found { + 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, @@ -74,16 +76,8 @@ func fileListDiff(ff1, ff2 []string, n1, n2 string) (mm []XmlMismatch) { } } - for _, p2 := range ff2 { - b2 := filepath.Base(p2) - found := false - for _, p1 := range ff1 { - if filepath.Base(p1) == b2 { - found = true - break - } - } - if !found { + for b2 := range map2 { + if _, ok := map1[b2]; !ok { m := XmlMismatch{ Path: b2, Type: XmlMismatchNewFile, diff --git a/zapdiff/pipeline.go b/zapdiff/pipeline.go index 18da8265..1c86ff79 100644 --- a/zapdiff/pipeline.go +++ b/zapdiff/pipeline.go @@ -35,12 +35,12 @@ func Pipeline(ff1, ff2 []string, n1, n2 string) (mm []XmlMismatch) { err := d1.ReadFromFile(f.p1) if err != nil { - slog.Warn("Failed to parse %s: %v\n", f.p1, err) + slog.Warn("Failed to parse", "file", f.p1, "error", err) continue } err = d2.ReadFromFile(f.p2) if err != nil { - slog.Warn("Failed to parse %s: %v\n", f.p2, err) + slog.Warn("Failed to parse", "file", f.p2, "error", err) continue } @@ -48,11 +48,11 @@ func Pipeline(ff1, ff2 []string, n1, n2 string) (mm []XmlMismatch) { r2 := d2.Root() if r1 == nil { - slog.Warn("File %s (%s) has no root element\n", baseName, n1) + slog.Warn("File has no root element", "file", baseName, "clone", n1) continue } if r2 == nil { - slog.Warn("File %s (%s) has no root element\n", baseName, n2) + slog.Warn("File has no root element", "file", baseName, "clone", n2) continue } From 5c93471336cc09974aa34bd7cd7b68f77cc6861f Mon Sep 17 00:00:00 2001 From: Arya Hassanli Date: Wed, 12 Nov 2025 16:13:00 +0000 Subject: [PATCH 5/6] Reduce number of levels * clean up cli --- cmd/cli/zapdiff.go | 16 +++++++------- zapdiff/xml_mismatch.go | 46 ++++++++++++++++++----------------------- 2 files changed, 27 insertions(+), 35 deletions(-) diff --git a/cmd/cli/zapdiff.go b/cmd/cli/zapdiff.go index bd8e9629..2eca9c49 100644 --- a/cmd/cli/zapdiff.go +++ b/cmd/cli/zapdiff.go @@ -8,26 +8,24 @@ import ( "sort" "strings" - "github.com/project-chip/alchemy/matter/spec" "github.com/project-chip/alchemy/sdk" "github.com/project-chip/alchemy/zapdiff" ) type ZAPDiff struct { - spec.FilterOptions `embed:""` - SdkRoot1 string `default:"connectedhomeip" help:"the first clone of project-chip/connectedhomeip" group:"SDK Commands:"` - SdkRoot2 string `default:"connectedhomeip" help:"the second clone of project-chip/connectedhomeip" 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-5)" group:"SDK Commands:"` + SdkRoot1 string `default:"connectedhomeip" help:"the first clone of project-chip/connectedhomeip" group:"SDK Commands:"` + SdkRoot2 string `default:"connectedhomeip" help:"the second clone of project-chip/connectedhomeip" 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 > 5 { - slog.Warn("invalid mismatch level. must be between 1 and 5.", "level", z.MismatchLevel) + 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-5 to 0-4 + mismatchPrintLevel = zapdiff.XmlMismatchLevel(z.MismatchLevel - 1) // Convert 1-3 to 0-2 } err = sdk.CheckAlchemyVersion(z.SdkRoot1) diff --git a/zapdiff/xml_mismatch.go b/zapdiff/xml_mismatch.go index 06063fea..f6f648f0 100644 --- a/zapdiff/xml_mismatch.go +++ b/zapdiff/xml_mismatch.go @@ -8,8 +8,6 @@ const ( MismatchLevel1 XmlMismatchLevel = iota MismatchLevel2 MismatchLevel3 - MismatchLevel4 - MismatchLevel5 ) func (l XmlMismatchLevel) String() string { @@ -20,10 +18,6 @@ func (l XmlMismatchLevel) String() string { return "L2" case MismatchLevel3: return "L3" - case MismatchLevel4: - return "L4" - case MismatchLevel5: - return "L5" default: return "UNKNOWN" @@ -188,70 +182,70 @@ func (t XmlMismatchType) Level() XmlMismatchLevel { // Enums case XmlMismatchMissingEnum: - return MismatchLevel4 + return MismatchLevel3 case XmlMismatchMissingEnumItem: - return MismatchLevel4 + return MismatchLevel3 case XmlMismatchEnumItemMissingAttr: return MismatchLevel1 case XmlMismatchEnumItemAttrValue: - return MismatchLevel4 + return MismatchLevel3 // Structs case XmlMismatchMissingStruct: - return MismatchLevel4 + return MismatchLevel3 case XmlMismatchMissingStructItem: - return MismatchLevel4 + return MismatchLevel3 case XmlMismatchStructItemMissingAttr: return MismatchLevel1 case XmlMismatchStructItemAttrValue: - return MismatchLevel4 + return MismatchLevel3 // Bitmaps case XmlMismatchMissingBitmap: - return MismatchLevel4 + return MismatchLevel3 case XmlMismatchMissingBitmapField: - return MismatchLevel4 + return MismatchLevel3 case XmlMismatchBitmapMissingAttr: return MismatchLevel1 case XmlMismatchBitmapAttrValue: - return MismatchLevel4 + return MismatchLevel3 case XmlMismatchBitmapFieldMissingAttr: return MismatchLevel1 case XmlMismatchBitmapFieldAttrValue: - return MismatchLevel4 + return MismatchLevel3 // Clusters (Top Level) case XmlMismatchMissingCluster: - return MismatchLevel4 + return MismatchLevel3 case XmlMismatchClusterMissingAttr: return MismatchLevel1 case XmlMismatchClusterAttrValue: - return MismatchLevel4 + return MismatchLevel3 // Clusters case XmlMismatchMissingClusterCommand: - return MismatchLevel4 + return MismatchLevel3 case XmlMismatchClusterCommandMissingAttr: return MismatchLevel1 case XmlMismatchClusterCommandAttrValue: - return MismatchLevel4 + return MismatchLevel3 case XmlMismatchMissingClusterAttribute: - return MismatchLevel4 + return MismatchLevel3 case XmlMismatchClusterAttributeMissingAttr: return MismatchLevel1 case XmlMismatchClusterAttributeAttrValue: - return MismatchLevel4 + return MismatchLevel3 case XmlMismatchMissingClusterEvent: - return MismatchLevel4 + return MismatchLevel3 case XmlMismatchClusterEventMissingAttr: return MismatchLevel1 case XmlMismatchClusterEventAttrValue: - return MismatchLevel4 + return MismatchLevel3 case XmlMismatchMissingClusterFeature: - return MismatchLevel4 + return MismatchLevel3 case XmlMismatchClusterDetails: - return MismatchLevel4 + return MismatchLevel3 default: return MismatchLevel1 From 788da5d2a21805cbcc35a59e668f2cd13fa87e98 Mon Sep 17 00:00:00 2001 From: Arya Hassanli Date: Thu, 20 Nov 2025 14:35:48 +0000 Subject: [PATCH 6/6] Clean up cli --- cmd/cli/zapdiff.go | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/cmd/cli/zapdiff.go b/cmd/cli/zapdiff.go index 2eca9c49..2c8936bd 100644 --- a/cmd/cli/zapdiff.go +++ b/cmd/cli/zapdiff.go @@ -8,15 +8,16 @@ import ( "sort" "strings" - "github.com/project-chip/alchemy/sdk" "github.com/project-chip/alchemy/zapdiff" ) type ZAPDiff struct { - SdkRoot1 string `default:"connectedhomeip" help:"the first clone of project-chip/connectedhomeip" group:"SDK Commands:"` - SdkRoot2 string `default:"connectedhomeip" help:"the second clone of project-chip/connectedhomeip" group:"SDK Commands:"` + 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:"` + MismatchLevel int `default:"3" help:"the minimum mismatch level to report (1-3)" group:"SDK Commands:"` } func (z *ZAPDiff) Run(cc *Context) (err error) { @@ -28,31 +29,19 @@ func (z *ZAPDiff) Run(cc *Context) (err error) { mismatchPrintLevel = zapdiff.XmlMismatchLevel(z.MismatchLevel - 1) // Convert 1-3 to 0-2 } - err = sdk.CheckAlchemyVersion(z.SdkRoot1) + ff1, err := listXMLFiles(z.XmlRoot1) if err != nil { - return - } - - err = sdk.CheckAlchemyVersion(z.SdkRoot2) - if err != nil { - return - } - - p1 := filepath.Join(z.SdkRoot1, "src", "app", "zap-templates", "zcl", "data-model", "chip") - ff1, err := listXMLFiles(p1) - if err != nil { - slog.Error("error listing files", "dir", p1, "error", err) + slog.Error("error listing files", "dir", z.XmlRoot1, "error", err) return err } - p2 := filepath.Join(z.SdkRoot2, "src", "app", "zap-templates", "zcl", "data-model", "chip") - ff2, err := listXMLFiles(p2) + ff2, err := listXMLFiles(z.XmlRoot2) if err != nil { - slog.Error("error listing files", "dir", p2, "error", err) + slog.Error("error listing files", "dir", z.XmlRoot2, "error", err) return err } - mm := zapdiff.Pipeline(ff1, ff2, "sdk-1", "sdk-2") + mm := zapdiff.Pipeline(ff1, ff2, z.Label1, z.Label2) csvOutputPath := filepath.Join(z.Out, "mismatches.csv") err = writeMismatchesToCSV(csvOutputPath, mm, mismatchPrintLevel)