package format import ( "os" "path/filepath" "strings" "testing" "git.warky.dev/wdevs/pgtidy/pkg/config" "git.warky.dev/wdevs/pgtidy/pkg/lexer" "git.warky.dev/wdevs/pgtidy/pkg/parser" ) func format(src string) string { return File(parser.Parse(src), config.Default()) } func TestFormatHeaderGolden(t *testing.T) { src := "--select * from dropall('resolvespec_login');\n" + "create or replace function resolvespec_login(\n" + "INOUT p_data jsonb, OUT p_success boolean, OUT p_error text)\n" + "language plpgsql volatile security definer\n" + "as $$\nbegin end;\n$$;\n" want := "--select * from dropall('resolvespec_login');\n" + "CREATE OR REPLACE FUNCTION resolvespec_login(\n" + " INOUT p_data jsonb\n" + " ,OUT p_success boolean\n" + " ,OUT p_error text\n" + ")\n" + "LANGUAGE plpgsql\n" + "VOLATILE\n" + "SECURITY DEFINER\n" + "AS\n" + "$$\nbegin end;\n$$;\n" got := format(src) if got != want { t.Errorf("header format mismatch\n--- got ---\n%s\n--- want ---\n%s", got, want) } } func TestIdempotentSmall(t *testing.T) { src := "create function f(a int,b text) returns void language sql as $$ select 1 $$;" once := format(src) twice := format(once) if once != twice { t.Errorf("not idempotent\n--- once ---\n%s\n--- twice ---\n%s", once, twice) } } func TestFormatBodyBroken(t *testing.T) { dir := filepath.Join("..", "..", "testdata", "corpus") brokenData, err := os.ReadFile(filepath.Join(dir, "test_a_broken.pgsql")) if err != nil { t.Skipf("no test_a_broken.pgsql: %v", err) } goldenData, err := os.ReadFile(filepath.Join(dir, "test_a.pgsql")) if err != nil { t.Skipf("no test_a.pgsql: %v", err) } got := format(string(brokenData)) want := string(goldenData) if got != want { t.Errorf("format(test_a_broken) != test_a.pgsql\n--- got ---\n%s\n--- want ---\n%s", got, want) } twice := format(got) if twice != got { t.Errorf("format(test_a_broken) is not idempotent") } } func TestFormatMmProcBroken(t *testing.T) { dir := filepath.Join("..", "..", "testdata", "corpus") brokenData, err := os.ReadFile(filepath.Join(dir, "test_mm_proc_broken.pgsql")) if err != nil { t.Skipf("no test_mm_proc_broken.pgsql: %v", err) } goldenData, err := os.ReadFile(filepath.Join(dir, "test_mm_proc.pgsql")) if err != nil { t.Skipf("no test_mm_proc.pgsql: %v", err) } got := format(string(brokenData)) want := string(goldenData) if got != want { // Find and report the first differing line. gotLines := strings.Split(got, "\n") wantLines := strings.Split(want, "\n") for i := 0; i < len(gotLines) && i < len(wantLines); i++ { if gotLines[i] != wantLines[i] { t.Errorf("format(test_mm_proc_broken) != test_mm_proc.pgsql at line %d\n got: %q\n want: %q", i+1, gotLines[i], wantLines[i]) break } } if len(gotLines) != len(wantLines) { t.Errorf("format(test_mm_proc_broken): got %d lines, want %d lines", len(gotLines), len(wantLines)) } } twice := format(got) if twice != got { t.Errorf("format(test_mm_proc_broken) is not idempotent") } if !semanticallyEqual(string(brokenData), got) { t.Errorf("format(test_mm_proc_broken) changed semantics") } } func TestCorpusIdempotentAndSafe(t *testing.T) { dir := filepath.Join("..", "..", "testdata", "corpus") entries, err := os.ReadDir(dir) if err != nil { t.Skipf("no corpus: %v", err) } var seen int for _, e := range entries { if e.IsDir() || !strings.HasSuffix(e.Name(), ".pgsql") || strings.HasSuffix(e.Name(), "_broken.pgsql") { continue } seen++ data, err := os.ReadFile(filepath.Join(dir, e.Name())) if err != nil { t.Fatal(err) } src := string(data) once := format(src) twice := format(once) if once != twice { t.Errorf("%s: not idempotent", e.Name()) } if !semanticallyEqual(src, once) { t.Errorf("%s: formatting changed semantics", e.Name()) } } if seen == 0 { t.Skip("no corpus files") } t.Logf("formatted %d corpus files (idempotent + semantically equal)", seen) } // semanticallyEqual compares the non-trivia token streams of two sources, // treating unquoted identifiers/keywords case-insensitively and everything // else (strings, numbers, operators, punctuation) exactly. Dollar-quoted body // tokens are compared recursively so body whitespace normalization does not // trigger a false failure. func semanticallyEqual(a, b string) bool { ta := significant(a) tb := significant(b) if len(ta) != len(tb) { return false } for i := range ta { if ta[i].Kind != tb[i].Kind { return false } switch ta[i].Kind { case lexer.Ident: if !strings.EqualFold(ta[i].Text, tb[i].Text) { return false } case lexer.DollarString: _, innerA, _, okA := splitDollarQuote(ta[i].Text) _, innerB, _, okB := splitDollarQuote(tb[i].Text) if okA != okB || (okA && !semanticallyEqual(innerA, innerB)) { return false } default: if ta[i].Text != tb[i].Text { return false } } } return true } func significant(src string) []lexer.Token { var out []lexer.Token for _, t := range lexer.Lex(src) { if t.Kind == lexer.EOF || t.IsTrivia() { continue } out = append(out, t) } return out }