Skip to content

Commit d1c2404

Browse files
authored
Merge pull request #63 from AryaHassanli/zapdiff-cli
Introduce ZAPDiff CLI
2 parents 2ecad6f + c444719 commit d1c2404

9 files changed

Lines changed: 865 additions & 0 deletions

File tree

cmd/cli.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ var commands struct {
1313
Format cli.Format `cmd:"" help:"disco ball Matter spec documents specified by the filename_pattern" group:"Spec Commands:"`
1414
Disco cli.Disco `cmd:"" help:"disco ball Matter spec documents specified by the filename_pattern" group:"Spec Commands:"`
1515
ZAP cli.ZAP `cmd:"" help:"transmute the Matter spec into ZAP templates, optionally filtered to the files specified by filename_pattern" group:"SDK Commands:"`
16+
ZAPDiff cli.ZAPDiff `cmd:"" name:"zap-diff" help:"Compares two set of ZAP XMLs for any inconsistency." group:"SDK Commands:"`
1617
MLE cli.MLE `cmd:"" help:"master list enforcer checks for inconsistencies between the master list and spec." group:"Spec Commands:"`
1718
Conformance cli.Conformance `cmd:"" help:"test conformance values" group:"Spec Commands:"`
1819
Dump dump.Command `cmd:"" hidden:"" help:"dump the parse tree of Matter documents specified by filename_pattern"`

cmd/cli/zapdiff.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package cli
2+
3+
import (
4+
"encoding/csv"
5+
"log/slog"
6+
"os"
7+
"path/filepath"
8+
"sort"
9+
"strings"
10+
11+
"github.com/project-chip/alchemy/zapdiff"
12+
)
13+
14+
type ZAPDiff struct {
15+
XmlRoot1 string `help:"root of first set of ZAP XMLs" group:"SDK Commands:" required:"true"`
16+
XmlRoot2 string `help:"root of second set of ZAP XMLs" group:"SDK Commands:" required:"true"`
17+
Label1 string `default:"ZapXML-1" help:"label for first set of ZAP XMLs" group:"SDK Commands:"`
18+
Label2 string `default:"ZapXML-2" help:"label for second set of ZAP XMLs" group:"SDK Commands:"`
19+
Out string `default:"." help:"path to output mismatch.csv file" group:"SDK Commands:"`
20+
MismatchLevel int `default:"3" help:"the minimum mismatch level to report (1-3)" group:"SDK Commands:"`
21+
}
22+
23+
func (z *ZAPDiff) Run(cc *Context) (err error) {
24+
var mismatchPrintLevel zapdiff.XmlMismatchLevel
25+
if z.MismatchLevel < 1 || z.MismatchLevel > 3 {
26+
slog.Warn("invalid mismatch level. must be between 1 and 3.", "level", z.MismatchLevel)
27+
mismatchPrintLevel = zapdiff.MismatchLevel3 // Default
28+
} else {
29+
mismatchPrintLevel = zapdiff.XmlMismatchLevel(z.MismatchLevel - 1) // Convert 1-3 to 0-2
30+
}
31+
32+
ff1, err := listXMLFiles(z.XmlRoot1)
33+
if err != nil {
34+
slog.Error("error listing files", "dir", z.XmlRoot1, "error", err)
35+
return err
36+
}
37+
38+
ff2, err := listXMLFiles(z.XmlRoot2)
39+
if err != nil {
40+
slog.Error("error listing files", "dir", z.XmlRoot2, "error", err)
41+
return err
42+
}
43+
44+
mm := zapdiff.Pipeline(ff1, ff2, z.Label1, z.Label2)
45+
46+
csvOutputPath := filepath.Join(z.Out, "mismatches.csv")
47+
err = writeMismatchesToCSV(csvOutputPath, mm, mismatchPrintLevel)
48+
if err != nil {
49+
slog.Error("Failed to write CSV output", "error", err)
50+
}
51+
52+
return
53+
}
54+
55+
func listXMLFiles(p string) (paths []string, err error) {
56+
var entries []os.DirEntry
57+
entries, err = os.ReadDir(p)
58+
if err != nil {
59+
return
60+
}
61+
62+
for _, e := range entries {
63+
if strings.HasSuffix(e.Name(), ".xml") {
64+
paths = append(paths, filepath.Join(p, e.Name()))
65+
}
66+
}
67+
68+
return
69+
}
70+
71+
func writeMismatchesToCSV(p string, mm []zapdiff.XmlMismatch, l zapdiff.XmlMismatchLevel) (err error) {
72+
f, err := os.Create(p)
73+
if err != nil {
74+
slog.Error("failed to create file", "path", p, "error", err)
75+
return err
76+
}
77+
defer f.Close()
78+
79+
w := csv.NewWriter(f)
80+
defer func() {
81+
w.Flush()
82+
if err == nil {
83+
err = w.Error()
84+
}
85+
}()
86+
87+
// Write header
88+
header := []string{"Level", "Type", "File", "Element Xpath", "Details"}
89+
if err = w.Write(header); err != nil {
90+
slog.Error("failed to write CSV header", "error", err)
91+
return
92+
}
93+
94+
sort.Slice(mm, func(i, j int) bool {
95+
// Level (Descending), Path, Type, ElementID, Details
96+
if mm[i].Level() != mm[j].Level() {
97+
return mm[i].Level() > mm[j].Level()
98+
}
99+
if mm[i].Path != mm[j].Path {
100+
return mm[i].Path < mm[j].Path
101+
}
102+
if mm[i].Type != mm[j].Type {
103+
return mm[i].Type.String() < mm[j].Type.String()
104+
}
105+
if mm[i].ElementID != mm[j].ElementID {
106+
return mm[i].ElementID < mm[j].ElementID
107+
}
108+
return mm[i].Details < mm[j].Details
109+
})
110+
111+
// Write mismatches
112+
for _, m := range mm {
113+
if m.Level() >= l {
114+
row := []string{
115+
m.Level().String(),
116+
m.Type.String(),
117+
m.Path,
118+
m.ElementID,
119+
m.Details,
120+
}
121+
if err = w.Write(row); err != nil {
122+
slog.Error("Warning: failed to write row to CSV", "err", err)
123+
return
124+
}
125+
}
126+
}
127+
128+
slog.Info("Successfully wrote mismatches to CSV", "dir", p)
129+
return
130+
}

zapdiff/check.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package zapdiff
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/beevik/etree"
7+
)
8+
9+
func checkMismatches(ep elementPair, baseName string, n1, n2 string) (mm []XmlMismatch) {
10+
e1Children := make(map[string]*etree.Element, len(ep.e1.ChildElements()))
11+
e2Children := make(map[string]*etree.Element, len(ep.e2.ChildElements()))
12+
mm = make([]XmlMismatch, 0)
13+
14+
for _, c1 := range ep.e1.ChildElements() {
15+
id := getElementID(c1)
16+
e1Children[id] = c1
17+
}
18+
19+
for _, c2 := range ep.e2.ChildElements() {
20+
id := getElementID(c2)
21+
e2Children[id] = c2
22+
}
23+
24+
for id, e1 := range e1Children {
25+
if _, ok := e2Children[id]; !ok {
26+
m := XmlMismatch{
27+
Path: baseName,
28+
Type: getMismatchMissingType(e1),
29+
Details: fmt.Sprintf("Only found in %s", n1),
30+
ElementID: id,
31+
}
32+
mm = append(mm, m)
33+
}
34+
}
35+
36+
for id, e2 := range e2Children {
37+
if _, ok := e1Children[id]; !ok {
38+
m := XmlMismatch{
39+
Path: baseName,
40+
Type: getMismatchMissingType(e2),
41+
Details: fmt.Sprintf("Only found in %s", n2),
42+
ElementID: id,
43+
}
44+
mm = append(mm, m)
45+
}
46+
}
47+
48+
// Recurse into common tags
49+
for id, e1 := range e1Children {
50+
if e2, ok := e2Children[id]; ok {
51+
// Check attributes
52+
attrMM := checkAttributes(elementPair{e1: e1, e2: e2}, id, baseName, n1, n2)
53+
mm = append(mm, attrMM...)
54+
55+
// Recurse
56+
subMM := checkMismatches(elementPair{e1: e1, e2: e2}, baseName, n1, n2)
57+
mm = append(mm, subMM...)
58+
}
59+
}
60+
61+
return
62+
}
63+
64+
func checkAttributes(ep elementPair, id string, baseName string, n1, n2 string) (mm []XmlMismatch) {
65+
mm = make([]XmlMismatch, 0)
66+
e1Attrs := make(map[string]string, len(ep.e1.Attr))
67+
e2Attrs := make(map[string]string, len(ep.e2.Attr))
68+
69+
for _, a := range ep.e1.Attr {
70+
e1Attrs[a.Key] = a.Value
71+
}
72+
for _, a := range ep.e2.Attr {
73+
e2Attrs[a.Key] = a.Value
74+
}
75+
76+
for k, v1 := range e1Attrs {
77+
if v2, ok := e2Attrs[k]; !ok {
78+
m := XmlMismatch{
79+
Path: baseName,
80+
Type: getMismatchMissingAttrType(ep.e1),
81+
Details: fmt.Sprintf("Attribute [%s] only found in %s", k, n1),
82+
ElementID: id,
83+
}
84+
mm = append(mm, m)
85+
} else if v1 != v2 {
86+
m := XmlMismatch{
87+
Path: baseName,
88+
Type: getMismatchAttrValueType(ep.e1),
89+
Details: fmt.Sprintf("Attribute [%s] has different values: '%s' in %s, '%s' in %s", k, v1, n1, v2, n2),
90+
ElementID: id,
91+
}
92+
mm = append(mm, m)
93+
}
94+
}
95+
96+
for k := range e2Attrs {
97+
if _, ok := e1Attrs[k]; !ok {
98+
m := XmlMismatch{
99+
Path: baseName,
100+
Type: getMismatchMissingAttrType(ep.e2),
101+
Details: fmt.Sprintf("Attribute [%s] only found in %s", k, n2),
102+
ElementID: id,
103+
}
104+
mm = append(mm, m)
105+
}
106+
}
107+
return
108+
}

zapdiff/element_id.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package zapdiff
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/beevik/etree"
7+
)
8+
9+
func parentAndSelfAttr(e *etree.Element, attr string) string {
10+
parentID := getElementID(e.Parent())
11+
a := e.SelectAttr(attr)
12+
if a == nil {
13+
return fmt.Sprintf("%s/%s", parentID, getElementXPathSegment(e))
14+
}
15+
return fmt.Sprintf("%s/%s[@%s='%s']", parentID, e.Tag, attr, a.Value)
16+
}
17+
18+
func parentAndSelfText(e *etree.Element) string {
19+
parentID := getElementID(e.Parent())
20+
text := e.Text()
21+
if text == "" {
22+
return fmt.Sprintf("%s/%s", parentID, getElementXPathSegment(e))
23+
}
24+
return fmt.Sprintf("%s/%s[text()='%s']", parentID, e.Tag, text)
25+
}
26+
27+
func getElementID(e *etree.Element) string {
28+
if e == nil {
29+
return ""
30+
}
31+
p := e.GetPath()
32+
33+
switch p {
34+
case "/configurator":
35+
return "configurator"
36+
case "/configurator/global/attribute",
37+
"/configurator/enum",
38+
"/configurator/enum/item",
39+
"/configurator/struct",
40+
"/configurator/struct/item",
41+
"/configurator/bitmap",
42+
"/configurator/bitmap/field",
43+
"/configurator/cluster/command",
44+
"/configurator/cluster/command/arg",
45+
"/configurator/cluster/attribute",
46+
"/configurator/cluster/event",
47+
"/configurator/cluster/event/field",
48+
"/configurator/cluster/features/feature":
49+
return parentAndSelfAttr(e, "name")
50+
case "/configurator/enum/cluster",
51+
"/configurator/struct/cluster":
52+
return parentAndSelfAttr(e, "code")
53+
case "/configurator/cluster":
54+
parentID := getElementID(e.Parent())
55+
code := e.SelectAttrValue("code", "")
56+
if code != "" {
57+
return fmt.Sprintf("%s/%s[@code='%s']", parentID, e.Tag, code)
58+
}
59+
nameEl := e.SelectElement("name")
60+
if nameEl != nil {
61+
nameText := nameEl.Text()
62+
return fmt.Sprintf("%s/%s[name='%s']", parentID, e.Tag, nameText)
63+
} else {
64+
return getElementXPathSegment(e)
65+
}
66+
case "/configurator/cluster/name",
67+
"/configurator/cluster/domain",
68+
"/configurator/cluster/description",
69+
"/configurator/cluster/code",
70+
"/configurator/cluster/define",
71+
"/configurator/cluster/client",
72+
"/configurator/cluster/server":
73+
return parentAndSelfText(e)
74+
75+
default:
76+
parentID := getElementID(e.Parent())
77+
selfSegment := getElementXPathSegment(e)
78+
return fmt.Sprintf("%s/%s", parentID, selfSegment)
79+
}
80+
}

0 commit comments

Comments
 (0)