package format import ( "os" "path/filepath" "strings" "testing" "github.com/hein/pgtidy/pkg/config" "github.com/hein/pgtidy/pkg/lexer" "github.com/hein/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 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") { 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, dollar bodies, operators, punctuation) exactly. This // validates that formatting changed only layout/casing, never meaning. 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 } if ta[i].Kind == lexer.Ident { if !strings.EqualFold(ta[i].Text, tb[i].Text) { return false } } else 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 }