Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:"`
Expand Down
137 changes: 137 additions & 0 deletions cmd/cli/zapdiff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package cli

import (
"encoding/csv"
"log/slog"
"os"
"path/filepath"
"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:"`
}

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
}

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 {
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
}
108 changes: 108 additions & 0 deletions zapdiff/check.go
Original file line number Diff line number Diff line change
@@ -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
}
69 changes: 69 additions & 0 deletions zapdiff/element_id.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading