Skip to content

Commit 0ba81dd

Browse files
authored
Merge pull request #64 from AryaHassanli/zapdiff
Introduce ZAPDiff Action and Refactor ZAPDiff CLI
2 parents d1c2404 + bcf9392 commit 0ba81dd

13 files changed

Lines changed: 1340 additions & 168 deletions

File tree

cmd/action/action.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ type Action struct {
55
Disco Disco `cmd:"" default:"" help:"GitHub action for Matter spec documents"`
66
ZAP ZAP `cmd:"" help:"GitHub action for Matter SDK ZAP XML"`
77
MergeGuard MergeGuard `cmd:"" help:"GitHub action to prevent Provisionality and Parse errors to be merged."`
8+
ZAPDiff ZAPDiff `cmd:"" help:"GitHub action for ZAP XML diff"`
89
}

cmd/action/zapdiff.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package action
2+
3+
import (
4+
"log/slog"
5+
6+
"github.com/project-chip/alchemy/cmd/cli"
7+
)
8+
9+
type ZAPDiffConfig struct {
10+
ComparisonSets []ComparisonSet `yaml:"comparison_sets"`
11+
}
12+
13+
type ComparisonSet struct {
14+
SDKRef string `yaml:"sdk_ref"`
15+
SDKLabel string `yaml:"sdk_label"`
16+
SpecRef string `yaml:"spec_ref"`
17+
SpecLabel string `yaml:"spec_label"`
18+
ZapGenerationAttributes string `yaml:"zap_generation_attributes"`
19+
AlchemyRef string `yaml:"alchemy_ref"`
20+
}
21+
22+
type ZAPDiff struct {
23+
BaselineXMLDir string `name:"baseline-xml" help:"Path to baseline XML file or directory" required:"true"`
24+
GeneratedXMLDir string `name:"generated-xml" help:"Path to generated XML file or directory" required:"true"`
25+
GeneratedSDKRoot string `name:"sdk-root" help:"Path to SDK root directory (for ZAP generation)" required:"true"`
26+
SDKLabel string `name:"sdk-label" help:"Label for SDK (used in human-readable reports)" default:"SDK"`
27+
SpecLabel string `name:"spec-label" help:"Label for Spec (used in human-readable reports)" default:"Spec"`
28+
GenAttributes string `name:"gen-attributes" help:"Zap generation attributes"`
29+
MismatchLevel int `name:"mismatch-level" help:"Mismatch level to report (1-3); 1 is most important" default:"3"`
30+
SpecRoot string `name:"spec-root" help:"Path to Spec root directory" default:"."`
31+
}
32+
33+
func (z *ZAPDiff) Run(cc *cli.Context) (err error) {
34+
slog.Info("Running ZAP generation", "attributes", z.GenAttributes)
35+
zapCmd := &cli.ZAP{}
36+
zapCmd.Root = z.SpecRoot
37+
zapCmd.SdkRoot = z.GeneratedSDKRoot
38+
zapCmd.Attribute = []string{z.GenAttributes}
39+
zapCmd.FeatureXML = true
40+
zapCmd.ConformanceXML = true
41+
zapCmd.NoProgress = true
42+
err = zapCmd.Run(cc)
43+
if err != nil {
44+
slog.Error("ZAP generation failed", "error", err)
45+
return err
46+
}
47+
48+
slog.Info("Running ZAPDiff", "xml1", z.BaselineXMLDir, "xml2", z.GeneratedXMLDir)
49+
diffCmd := &cli.ZAPDiff{}
50+
diffCmd.XmlRoot1 = z.BaselineXMLDir
51+
diffCmd.XmlRoot2 = z.GeneratedXMLDir
52+
diffCmd.Label1 = z.SDKLabel
53+
diffCmd.Label2 = z.SpecLabel
54+
diffCmd.MismatchLevel = z.MismatchLevel
55+
diffCmd.Format = "both"
56+
57+
err = diffCmd.Run(cc)
58+
if err != nil {
59+
slog.Error("ZAPDiff failed", "error", err)
60+
return err
61+
}
62+
63+
slog.Info("zapdiff Action completed successfully")
64+
return nil
65+
}

cmd/cli/zapdiff.go

Lines changed: 41 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
package cli
22

33
import (
4-
"encoding/csv"
4+
"io"
55
"log/slog"
66
"os"
77
"path/filepath"
8-
"sort"
98
"strings"
109

1110
"github.com/project-chip/alchemy/zapdiff"
@@ -16,17 +15,19 @@ type ZAPDiff struct {
1615
XmlRoot2 string `help:"root of second set of ZAP XMLs" group:"SDK Commands:" required:"true"`
1716
Label1 string `default:"ZapXML-1" help:"label for first set of ZAP XMLs" group:"SDK Commands:"`
1817
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:"`
18+
Out string `default:"." help:"path to output mismatch files" group:"SDK Commands:"`
19+
Format string `default:"both" help:"output format: csv, html, or both" group:"SDK Commands:"`
2020
MismatchLevel int `default:"3" help:"the minimum mismatch level to report (1-3)" group:"SDK Commands:"`
2121
}
2222

2323
func (z *ZAPDiff) Run(cc *Context) (err error) {
24+
2425
var mismatchPrintLevel zapdiff.XmlMismatchLevel
2526
if z.MismatchLevel < 1 || z.MismatchLevel > 3 {
2627
slog.Warn("invalid mismatch level. must be between 1 and 3.", "level", z.MismatchLevel)
2728
mismatchPrintLevel = zapdiff.MismatchLevel3 // Default
2829
} else {
29-
mismatchPrintLevel = zapdiff.XmlMismatchLevel(z.MismatchLevel - 1) // Convert 1-3 to 0-2
30+
mismatchPrintLevel = zapdiff.XmlMismatchLevel(3 - z.MismatchLevel)
3031
}
3132

3233
ff1, err := listXMLFiles(z.XmlRoot1)
@@ -43,16 +44,37 @@ func (z *ZAPDiff) Run(cc *Context) (err error) {
4344

4445
mm := zapdiff.Pipeline(ff1, ff2, z.Label1, z.Label2)
4546

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)
47+
generateCSV := z.Format == "csv" || z.Format == "both" || z.Format == ""
48+
if generateCSV {
49+
err = writeMismatchesFile(z.Out, "mismatches.csv", "CSV", func(w io.Writer) error {
50+
return zapdiff.WriteMismatchesToCSV(w, mm, mismatchPrintLevel)
51+
})
52+
if err != nil {
53+
return err
54+
}
5055
}
5156

52-
return
57+
generateHTML := z.Format == "html" || z.Format == "both" || z.Format == ""
58+
if generateHTML {
59+
err = writeMismatchesFile(z.Out, "mismatches.html", "HTML", func(w io.Writer) error {
60+
return zapdiff.WriteMismatchesToHTML(w, mm, mismatchPrintLevel, z.XmlRoot1, z.XmlRoot2)
61+
})
62+
if err != nil {
63+
return err
64+
}
65+
}
66+
67+
return nil
5368
}
5469

5570
func listXMLFiles(p string) (paths []string, err error) {
71+
fi, err := os.Stat(p)
72+
if err != nil {
73+
return nil, err
74+
}
75+
if !fi.IsDir() {
76+
return []string{p}, nil
77+
}
5678
var entries []os.DirEntry
5779
entries, err = os.ReadDir(p)
5880
if err != nil {
@@ -68,63 +90,19 @@ func listXMLFiles(p string) (paths []string, err error) {
6890
return
6991
}
7092

71-
func writeMismatchesToCSV(p string, mm []zapdiff.XmlMismatch, l zapdiff.XmlMismatchLevel) (err error) {
72-
f, err := os.Create(p)
93+
func writeMismatchesFile(outDir string, filename string, formatName string, writeFn func(io.Writer) error) error {
94+
outputPath := filepath.Join(outDir, filename)
95+
f, err := os.Create(outputPath)
7396
if err != nil {
74-
slog.Error("failed to create file", "path", p, "error", err)
97+
slog.Error("failed to create "+formatName+" file", "path", outputPath, "error", err)
7598
return err
7699
}
77100
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-
}
101+
err = writeFn(f)
102+
if err != nil {
103+
slog.Error("Failed to write "+formatName+" output", "error", err)
104+
} else {
105+
slog.Info("Successfully wrote mismatches to "+formatName, "dir", outputPath)
126106
}
127-
128-
slog.Info("Successfully wrote mismatches to CSV", "dir", p)
129-
return
107+
return nil
130108
}

cmd/github.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ var commands struct {
1313
Disco action.Disco `cmd:"" name:"disco" default:"" help:"Disco ball Matter spec documents specified by the filename_pattern" group:"Spec Actions:"`
1414
ZAP action.ZAP `cmd:"" help:"Transmute the Matter spec into ZAP templates, optionally filtered to the files specified by filename_pattern" group:"SDK Actions:"`
1515
MergeGuard action.MergeGuard `cmd:"" help:"GitHub action for checking Provisionality and Parse errors."`
16+
ZAPDiff action.ZAPDiff `cmd:"" name:"zap-diff" help:"GitHub action for ZAP XML diff"`
1617

1718
Version Version `cmd:"" hidden:"" name:"version" help:"display version number"`
1819

zapdiff/check.go

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,22 @@ func checkMismatches(ep elementPair, baseName string, n1, n2 string) (mm []XmlMi
1212
mm = make([]XmlMismatch, 0)
1313

1414
for _, c1 := range ep.e1.ChildElements() {
15-
id := getElementID(c1)
15+
id := getEntityUniqueIdentifier(c1)
1616
e1Children[id] = c1
1717
}
1818

1919
for _, c2 := range ep.e2.ChildElements() {
20-
id := getElementID(c2)
20+
id := getEntityUniqueIdentifier(c2)
2121
e2Children[id] = c2
2222
}
2323

2424
for id, e1 := range e1Children {
2525
if _, ok := e2Children[id]; !ok {
2626
m := XmlMismatch{
27-
Path: baseName,
28-
Type: getMismatchMissingType(e1),
29-
Details: fmt.Sprintf("Only found in %s", n1),
30-
ElementID: id,
27+
Path: baseName,
28+
Type: getMismatchMissingType(e1),
29+
Details: fmt.Sprintf("Only found in %s", n1),
30+
EntityUniqueIdentifier: id,
3131
}
3232
mm = append(mm, m)
3333
}
@@ -36,10 +36,10 @@ func checkMismatches(ep elementPair, baseName string, n1, n2 string) (mm []XmlMi
3636
for id, e2 := range e2Children {
3737
if _, ok := e1Children[id]; !ok {
3838
m := XmlMismatch{
39-
Path: baseName,
40-
Type: getMismatchMissingType(e2),
41-
Details: fmt.Sprintf("Only found in %s", n2),
42-
ElementID: id,
39+
Path: baseName,
40+
Type: getMismatchMissingType(e2),
41+
Details: fmt.Sprintf("Only found in %s", n2),
42+
EntityUniqueIdentifier: id,
4343
}
4444
mm = append(mm, m)
4545
}
@@ -76,18 +76,18 @@ func checkAttributes(ep elementPair, id string, baseName string, n1, n2 string)
7676
for k, v1 := range e1Attrs {
7777
if v2, ok := e2Attrs[k]; !ok {
7878
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,
79+
Path: baseName,
80+
Type: getMismatchMissingAttrType(ep.e1),
81+
Details: fmt.Sprintf("Attribute [%s] only found in %s", k, n1),
82+
EntityUniqueIdentifier: id,
8383
}
8484
mm = append(mm, m)
8585
} else if v1 != v2 {
8686
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,
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+
EntityUniqueIdentifier: id,
9191
}
9292
mm = append(mm, m)
9393
}
@@ -96,10 +96,10 @@ func checkAttributes(ep elementPair, id string, baseName string, n1, n2 string)
9696
for k := range e2Attrs {
9797
if _, ok := e1Attrs[k]; !ok {
9898
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,
99+
Path: baseName,
100+
Type: getMismatchMissingAttrType(ep.e2),
101+
Details: fmt.Sprintf("Attribute [%s] only found in %s", k, n2),
102+
EntityUniqueIdentifier: id,
103103
}
104104
mm = append(mm, m)
105105
}

zapdiff/csv.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package zapdiff
2+
3+
import (
4+
"encoding/csv"
5+
"io"
6+
"log/slog"
7+
"sort"
8+
)
9+
10+
// WriteMismatchesToCSV writes the given mismatches to the writer in CSV format.
11+
func WriteMismatchesToCSV(w io.Writer, mm []XmlMismatch, l XmlMismatchLevel) (err error) {
12+
csvWriter := csv.NewWriter(w)
13+
defer func() {
14+
csvWriter.Flush()
15+
if err == nil {
16+
err = csvWriter.Error()
17+
}
18+
}()
19+
20+
// Write header
21+
header := []string{"Level", "Type", "File", "Element Xpath", "Details"}
22+
if err = csvWriter.Write(header); err != nil {
23+
slog.Error("failed to write CSV header", "error", err)
24+
return
25+
}
26+
27+
sort.Slice(mm, func(i, j int) bool {
28+
// Level (Descending), Path, Type, EntityUniqueIdentifier, Details
29+
if mm[i].Level() != mm[j].Level() {
30+
return mm[i].Level() > mm[j].Level()
31+
}
32+
if mm[i].Path != mm[j].Path {
33+
return mm[i].Path < mm[j].Path
34+
}
35+
if mm[i].Type != mm[j].Type {
36+
return mm[i].Type.String() < mm[j].Type.String()
37+
}
38+
if mm[i].EntityUniqueIdentifier != mm[j].EntityUniqueIdentifier {
39+
return mm[i].EntityUniqueIdentifier < mm[j].EntityUniqueIdentifier
40+
}
41+
return mm[i].Details < mm[j].Details
42+
})
43+
44+
// Write mismatches
45+
for _, m := range mm {
46+
if m.Level() >= l {
47+
row := []string{
48+
m.Level().String(),
49+
m.Type.String(),
50+
m.Path,
51+
m.EntityUniqueIdentifier,
52+
m.Details,
53+
}
54+
if err = csvWriter.Write(row); err != nil {
55+
slog.Error("Warning: failed to write row to CSV", "err", err)
56+
return
57+
}
58+
}
59+
}
60+
61+
return
62+
}

0 commit comments

Comments
 (0)