Skip to content
quarry Artifact [3a0cd4ea5d]
Login

Artifact [3a0cd4ea5d]

Artifact 3a0cd4ea5db38d80193248ceea0ba24ad89fb48623237abb96a668b77188d30e:


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)
	}
}