119 lines
2.9 KiB
Go
119 lines
2.9 KiB
Go
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
|
|
}
|