Artifact 3a0cd4ea5db38d80193248ceea0ba24ad89fb48623237abb96a668b77188d30e:
- File
internal/semdiff/semdiff_test.go
— part of check-in
[b9d8e3692c]
at
2026-08-14 20:40:45
on branch trunk
— semdiff core + fixture corpus tooling
Canonical DOM/JSON/timestamp diff (0001 §11); synthetic fixture via fossil serve-and-clone; make fixtures / fixtures-real. (user: dmestas, size: 3358) [annotate] [blame] [check-ins using]
package semdiff import ( "strings" "testing" ) // --- JSON canonicalization --- func TestCanonJSONSortsKeys(t *testing.T) { a, err := CanonJSON([]byte(`{"b":1,"a":2}`)) if err != nil { t.Fatal(err) } b, err := CanonJSON([]byte(`{"a":2,"b":1}`)) if err != nil { t.Fatal(err) } if string(a) != string(b) { t.Fatalf("key order changed canon form:\n%s\n%s", a, b) } } func TestCanonJSONNormalizesTimestamps(t *testing.T) { // Same instant, different representations. a, err := CanonJSON([]byte(`{"t":"2026-08-14T10:00:00+02:00"}`)) if err != nil { t.Fatal(err) } b, err := CanonJSON([]byte(`{"t":"2026-08-14 08:00:00Z"}`)) if err != nil { t.Fatal(err) } if string(a) != string(b) { t.Fatalf("timestamps not unified:\n%s\n%s", a, b) } if !strings.Contains(string(a), "2026-08-14T08:00:00Z") { t.Fatalf("want UTC RFC3339, got %s", a) } } func TestCanonJSONLeavesNonTimestampsAlone(t *testing.T) { out, err := CanonJSON([]byte(`{"v":"1.2.3","n":42,"s":"2026 was a year"}`)) if err != nil { t.Fatal(err) } for _, want := range []string{`"1.2.3"`, `42`, `"2026 was a year"`} { if !strings.Contains(string(out), want) { t.Fatalf("mangled %s in %s", want, out) } } } func TestCanonJSONRejectsInvalid(t *testing.T) { if _, err := CanonJSON([]byte(`{nope`)); err == nil { t.Fatal("want parse error, got nil") } } // --- HTML canonicalization --- func TestCanonHTMLAttributeOrder(t *testing.T) { a, err := CanonHTML([]byte(`<div id="x" class="y">hi</div>`)) if err != nil { t.Fatal(err) } b, err := CanonHTML([]byte(`<div class="y" id="x">hi</div>`)) if err != nil { t.Fatal(err) } if string(a) != string(b) { t.Fatalf("attr order changed canon form:\n%s\n%s", a, b) } } func TestCanonHTMLWhitespace(t *testing.T) { a, err := CanonHTML([]byte("<p>hello world</p>")) if err != nil { t.Fatal(err) } b, err := CanonHTML([]byte("<p>\n hello world\n</p>")) if err != nil { t.Fatal(err) } if string(a) != string(b) { t.Fatalf("whitespace changed canon form:\n%q\n%q", a, b) } } func TestCanonHTMLPreservesPre(t *testing.T) { a, err := CanonHTML([]byte("<pre>a b\nc</pre>")) if err != nil { t.Fatal(err) } b, err := CanonHTML([]byte("<pre>a b c</pre>")) if err != nil { t.Fatal(err) } if string(a) == string(b) { t.Fatal("pre content collapsed — code blocks must stay byte-exact") } } func TestCanonHTMLContentDifferenceSurvives(t *testing.T) { a, _ := CanonHTML([]byte(`<p>alpha</p>`)) b, _ := CanonHTML([]byte(`<p>beta</p>`)) if string(a) == string(b) { t.Fatal("distinct content canonicalized to same form") } } // --- Diff --- func TestDiffEqualAfterCanon(t *testing.T) { d, err := Diff(JSON, []byte(`{"b":1,"a":2}`), []byte(`{"a":2,"b":1}`)) if err != nil { t.Fatal(err) } if d != "" { t.Fatalf("want no diff, got:\n%s", d) } } func TestDiffReportsFirstDivergence(t *testing.T) { d, err := Diff(JSON, []byte(`{"a":1}`), []byte(`{"a":2}`)) if err != nil { t.Fatal(err) } if d == "" || !strings.Contains(d, "1") || !strings.Contains(d, "2") { t.Fatalf("diff should show both sides, got:\n%s", d) } } func TestDiffHTMLKind(t *testing.T) { d, err := Diff(HTML, []byte(`<div class="y" id="x">a</div>`), []byte(`<div id="x" class="y">a</div>`)) if err != nil { t.Fatal(err) } if d != "" { t.Fatalf("want no diff, got:\n%s", d) } }