Skip to content

Commit 08bce31

Browse files
committed
DRYing output generation
1 parent e82e449 commit 08bce31

4 files changed

Lines changed: 345 additions & 40 deletions

File tree

cmd/action/zapdiff.go

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,25 @@ type ZAPDiff struct {
2323
BaselineXMLDir string `name:"baseline-xml" help:"Path to baseline XML file or directory" required:"true"`
2424
GeneratedXMLDir string `name:"generated-xml" help:"Path to generated XML file or directory" required:"true"`
2525
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" default:"SDK"`
27-
SpecLabel string `name:"spec-label" help:"Label for Spec" default:"Spec"`
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"`
2828
GenAttributes string `name:"gen-attributes" help:"Zap generation attributes"`
29-
MismatchLevel int `name:"mismatch-level" help:"Mismatch level to report (1-3)" default:"3"`
29+
MismatchLevel int `name:"mismatch-level" help:"Mismatch level to report (1-3); 1 is most important" default:"3"`
3030
}
3131

3232
func (z *ZAPDiff) Run(cc *cli.Context) (err error) {
33-
if z.GenAttributes != "" {
34-
slog.Info("Running ZAP generation", "attributes", z.GenAttributes)
35-
zapCmd := &cli.ZAP{}
36-
zapCmd.Root = "."
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-
}
33+
slog.Info("Running ZAP generation", "attributes", z.GenAttributes)
34+
zapCmd := &cli.ZAP{}
35+
zapCmd.Root = "."
36+
zapCmd.SdkRoot = z.GeneratedSDKRoot
37+
zapCmd.Attribute = []string{z.GenAttributes}
38+
zapCmd.FeatureXML = true
39+
zapCmd.ConformanceXML = true
40+
zapCmd.NoProgress = true
41+
err = zapCmd.Run(cc)
42+
if err != nil {
43+
slog.Error("ZAP generation failed", "error", err)
44+
return err
4745
}
4846

4947
slog.Info("Running ZAPDiff", "xml1", z.BaselineXMLDir, "xml2", z.GeneratedXMLDir)
@@ -53,6 +51,7 @@ func (z *ZAPDiff) Run(cc *cli.Context) (err error) {
5351
diffCmd.Label1 = z.SDKLabel
5452
diffCmd.Label2 = z.SpecLabel
5553
diffCmd.MismatchLevel = z.MismatchLevel
54+
diffCmd.Format = "both"
5655

5756
err = diffCmd.Run(cc)
5857
if err != nil {

cmd/cli/zapdiff.go

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cli
22

33
import (
4+
"io"
45
"log/slog"
56
"os"
67
"path/filepath"
@@ -44,41 +45,26 @@ func (z *ZAPDiff) Run(cc *Context) (err error) {
4445
mm := zapdiff.Pipeline(ff1, ff2, z.Label1, z.Label2)
4546

4647
generateCSV := z.Format == "csv" || z.Format == "both" || z.Format == ""
47-
generateHTML := z.Format == "html" || z.Format == "both" || z.Format == ""
48-
4948
if generateCSV {
50-
csvOutputPath := filepath.Join(z.Out, "mismatches.csv")
51-
f, err := os.Create(csvOutputPath)
49+
err = writeMismatchesFile(z.Out, "mismatches.csv", "CSV", func(w io.Writer) error {
50+
return zapdiff.WriteMismatchesToCSV(w, mm, mismatchPrintLevel)
51+
})
5252
if err != nil {
53-
slog.Error("failed to create CSV file", "path", csvOutputPath, "error", err)
5453
return err
5554
}
56-
defer f.Close()
57-
err = zapdiff.WriteMismatchesToCSV(f, mm, mismatchPrintLevel)
58-
if err != nil {
59-
slog.Error("Failed to write CSV output", "error", err)
60-
} else {
61-
slog.Info("Successfully wrote mismatches to CSV", "dir", csvOutputPath)
62-
}
6355
}
6456

57+
generateHTML := z.Format == "html" || z.Format == "both" || z.Format == ""
6558
if generateHTML {
66-
htmlOutputPath := filepath.Join(z.Out, "mismatches.html")
67-
f, err := os.Create(htmlOutputPath)
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+
})
6862
if err != nil {
69-
slog.Error("failed to create HTML file", "path", htmlOutputPath, "error", err)
7063
return err
7164
}
72-
defer f.Close()
73-
err = zapdiff.WriteMismatchesToHTML(f, mm, mismatchPrintLevel, z.XmlRoot1, z.XmlRoot2)
74-
if err != nil {
75-
slog.Error("Failed to write HTML output", "error", err)
76-
} else {
77-
slog.Info("Successfully wrote mismatches to HTML", "dir", htmlOutputPath)
78-
}
7965
}
8066

81-
return
67+
return nil
8268
}
8369

8470
func listXMLFiles(p string) (paths []string, err error) {
@@ -103,3 +89,20 @@ func listXMLFiles(p string) (paths []string, err error) {
10389

10490
return
10591
}
92+
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)
96+
if err != nil {
97+
slog.Error("failed to create "+formatName+" file", "path", outputPath, "error", err)
98+
return err
99+
}
100+
defer f.Close()
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)
106+
}
107+
return nil
108+
}

zapdiff/diff_test.go

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package zapdiff
2+
3+
import (
4+
"os"
5+
"strings"
6+
"testing"
7+
)
8+
9+
func TestParseEntityUniqueIdentifierWithText(t *testing.T) {
10+
id := "configurator/cluster[name='Test']/domain[text()='General']"
11+
segments, err := parseEntityUniqueIdentifier(id)
12+
if err != nil {
13+
t.Fatal(err)
14+
}
15+
16+
if len(segments) != 3 {
17+
t.Fatalf("expected 3 segments, got %d", len(segments))
18+
}
19+
20+
if segments[2].tag != "domain" {
21+
t.Errorf("expected tag domain, got %s", segments[2].tag)
22+
}
23+
if segments[2].attr != "text()" {
24+
t.Errorf("expected attr text(), got %s", segments[2].attr)
25+
}
26+
if segments[2].value != "General" {
27+
t.Errorf("expected value General, got %s", segments[2].value)
28+
}
29+
if segments[2].isAttr {
30+
t.Errorf("expected isAttr false, got true")
31+
}
32+
}
33+
34+
func TestFindElementLinesWithText(t *testing.T) {
35+
content := `<configurator>
36+
<cluster>
37+
<name>Test</name>
38+
<domain>General</domain>
39+
<domain>Special</domain>
40+
</cluster>
41+
</configurator>`
42+
43+
tmpFile, err := os.CreateTemp("", "zapdiff_test_*.xml")
44+
if err != nil {
45+
t.Fatal(err)
46+
}
47+
defer os.Remove(tmpFile.Name())
48+
49+
if _, err := tmpFile.WriteString(content); err != nil {
50+
t.Fatal(err)
51+
}
52+
tmpFile.Close()
53+
54+
id := "configurator/cluster[name='Test']/domain[text()='Special']"
55+
lines, startLine, err := findElementLines(tmpFile.Name(), id)
56+
if err != nil {
57+
t.Fatal(err)
58+
}
59+
60+
if len(lines) != 1 {
61+
t.Fatalf("expected 1 line, got %d", len(lines))
62+
}
63+
64+
if startLine != 5 { // 1-indexed, line 5 is "<domain>Special</domain>"
65+
t.Errorf("expected startLine 5, got %d", startLine)
66+
}
67+
68+
if !strings.Contains(lines[0], "<domain>Special</domain>") {
69+
t.Errorf("expected line to contain <domain>Special</domain>, got %s", lines[0])
70+
}
71+
}
72+
73+
func TestFindElementLinesWithChildAttributes(t *testing.T) {
74+
content := `<configurator>
75+
<cluster>
76+
<name>Test</name>
77+
<domain attr="val">Special</domain>
78+
</cluster>
79+
</configurator>`
80+
81+
tmpFile, err := os.CreateTemp("", "zapdiff_test_*.xml")
82+
if err != nil {
83+
t.Fatal(err)
84+
}
85+
defer os.Remove(tmpFile.Name())
86+
87+
if _, err := tmpFile.WriteString(content); err != nil {
88+
t.Fatal(err)
89+
}
90+
tmpFile.Close()
91+
92+
id := "configurator/cluster[domain='Special']"
93+
lines, startLine, err := findElementLines(tmpFile.Name(), id)
94+
if err != nil {
95+
t.Fatal(err)
96+
}
97+
98+
if len(lines) != 4 {
99+
t.Fatalf("expected 4 lines, got %d", len(lines))
100+
}
101+
102+
if startLine != 2 {
103+
t.Errorf("expected startLine 2, got %d", startLine)
104+
}
105+
}
106+
107+
func TestFindElementLinesWithMultiLineSelfClosing(t *testing.T) {
108+
content := `<configurator>
109+
<cluster name="Test"
110+
code="0x1234"
111+
/>
112+
</configurator>`
113+
114+
tmpFile, err := os.CreateTemp("", "zapdiff_test_*.xml")
115+
if err != nil {
116+
t.Fatal(err)
117+
}
118+
defer os.Remove(tmpFile.Name())
119+
120+
if _, err := tmpFile.WriteString(content); err != nil {
121+
t.Fatal(err)
122+
}
123+
tmpFile.Close()
124+
125+
id := "configurator/cluster[@name='Test']"
126+
lines, startLine, err := findElementLines(tmpFile.Name(), id)
127+
if err != nil {
128+
t.Fatal(err)
129+
}
130+
131+
if len(lines) != 3 {
132+
t.Fatalf("expected 3 lines, got %d", len(lines))
133+
}
134+
135+
if startLine != 2 {
136+
t.Errorf("expected startLine 2, got %d", startLine)
137+
}
138+
}
139+
140+
func TestGenerateUnifiedDiff(t *testing.T) {
141+
lines1 := []string{"line1\n", "line2\n", "line3\n"}
142+
lines2 := []string{"line1\n", "line2 modified\n", "line3\n"}
143+
144+
diffStr, err := GenerateUnifiedDiff(lines1, lines2, 10, 20)
145+
if err != nil {
146+
t.Fatal(err)
147+
}
148+
149+
expected := "--- Ref\n+++ Generated\n@@ -10,3 +20,3 @@\n line1\n-line2\n+line2 modified\n line3\n"
150+
if diffStr != expected {
151+
t.Errorf("expected diff:\n%s\ngot:\n%s", expected, diffStr)
152+
}
153+
}
154+
155+
func TestGenerateUnifiedDiffGrouped(t *testing.T) {
156+
lines1 := []string{"A\n", "B\n", "C\n", "D\n"}
157+
lines2 := []string{"A\n", "X\n", "Y\n", "D\n"}
158+
159+
diffStr, err := GenerateUnifiedDiff(lines1, lines2, 10, 20)
160+
if err != nil {
161+
t.Fatal(err)
162+
}
163+
164+
expected := "--- Ref\n+++ Generated\n@@ -10,4 +20,4 @@\n A\n-B\n-C\n+X\n+Y\n D\n"
165+
if diffStr != expected {
166+
t.Errorf("expected diff:\n%s\ngot:\n%s", expected, diffStr)
167+
}
168+
}
169+
170+
func TestGetCustomDiffLines(t *testing.T) {
171+
content := `<configurator>
172+
<cluster code="0x0028"/>
173+
<attribute code="0x0017" side="server"/>
174+
</configurator>`
175+
176+
tmpFile, err := os.CreateTemp("", "test_diff_*.xml")
177+
if err != nil {
178+
t.Fatal(err)
179+
}
180+
defer os.Remove(tmpFile.Name())
181+
182+
if _, err := tmpFile.WriteString(content); err != nil {
183+
t.Fatal(err)
184+
}
185+
tmpFile.Close()
186+
187+
// We need to mock getParentID or use a targetID that works with it.
188+
// getParentID removes the last segment.
189+
// If targetID is "configurator/cluster[@code='0x0028']/attribute[@code='0x0017']"
190+
// parentID will be "configurator/cluster[@code='0x0028']"
191+
// This matches the structure in our content (textually for findElementLines).
192+
193+
targetID := "configurator/cluster[@code='0x0028']/attribute[@code='0x0017']"
194+
195+
lines, _, err := getCustomDiffLines(tmpFile.Name(), targetID)
196+
if err != nil {
197+
t.Fatal(err)
198+
}
199+
200+
// Expected lines:
201+
// 1. Parent start: <cluster code="0x0028"/>
202+
// 2. Target: <attribute code="0x0017" side="server"/>
203+
// Parent close should NOT be appended because it was self-closing (len=1).
204+
205+
if len(lines) != 2 {
206+
t.Fatalf("expected 2 lines, got %d: %v", len(lines), lines)
207+
}
208+
209+
if !strings.Contains(lines[0], "<cluster") {
210+
t.Errorf("expected cluster line, got %s", lines[0])
211+
}
212+
if !strings.Contains(lines[1], "<attribute") {
213+
t.Errorf("expected attribute line, got %s", lines[1])
214+
}
215+
}

0 commit comments

Comments
 (0)