test: expand coverage across readers, writers, cmd, ui, diff and merge

Implements tests/_plans and previously deferred packages; updates plan
README with new coverage numbers.
This commit is contained in:
2026-10-03 21:33:59 +02:00
parent a32647ee16
commit 495a21b67b
50 changed files with 8461 additions and 8 deletions
+48
View File
@@ -0,0 +1,48 @@
package template
import (
"errors"
"strings"
"testing"
)
func TestTemplateError(t *testing.T) {
cause := errors.New("boom")
tests := []struct {
name string
err *TemplateError
phase string
}{
{"load", NewTemplateLoadError("cannot read", cause), "load"},
{"parse", NewTemplateParseError("bad syntax", cause), "parse"},
{"execute", NewTemplateExecuteError("failed render", cause), "execute"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.err.Phase != tt.phase {
t.Errorf("phase = %q", tt.err.Phase)
}
msg := tt.err.Error()
if !strings.Contains(msg, "template "+tt.phase+" error") || !strings.Contains(msg, "boom") {
t.Errorf("message = %q", msg)
}
if !errors.Is(tt.err, cause) {
t.Error("errors.Is must reach cause")
}
var te *TemplateError
if !errors.As(error(tt.err), &te) || te != tt.err {
t.Error("errors.As failed")
}
})
}
}
func TestTemplateErrorWithoutCause(t *testing.T) {
e := NewTemplateParseError("only message", nil)
if got := e.Error(); got != "template parse error: only message" {
t.Errorf("got %q", got)
}
if e.Unwrap() != nil {
t.Error("Unwrap must be nil")
}
}
+168
View File
@@ -0,0 +1,168 @@
package template
import (
"sort"
"testing"
"git.warky.dev/wdevs/relspecgo/pkg/models"
)
func colNames(cols []*models.Column) []string {
out := make([]string, 0, len(cols))
for _, c := range cols {
out = append(out, c.Name)
}
sort.Strings(out)
return out
}
func eqStrings(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func testColumns() map[string]*models.Column {
return map[string]*models.Column{
"id": {Name: "id", Type: "integer", IsPrimaryKey: true, NotNull: true},
"user_id": {Name: "user_id", Type: "bigint", NotNull: true},
"name": {Name: "name", Type: "varchar(50)"},
"email": {Name: "email", Type: "varchar(255)", NotNull: true},
"created_at": {Name: "created_at", Type: "timestamp"},
}
}
func TestFilterTables(t *testing.T) {
tables := []*models.Table{{Name: "user_profile"}, {Name: "user_settings"}, {Name: "orders"}}
tests := []struct {
name string
in []*models.Table
pattern string
want []string
}{
{"empty pattern returns all", tables, "", []string{"user_profile", "user_settings", "orders"}},
{"glob", tables, "user_*", []string{"user_profile", "user_settings"}},
{"single char", tables, "order?", []string{"orders"}},
{"no match", tables, "zzz*", []string{}},
{"nil input", nil, "x*", []string{}},
{"invalid pattern falls back to exact", []*models.Table{{Name: "[a"}}, "[a", []string{"[a"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := FilterTables(tt.in, tt.pattern)
names := []string{}
for _, tbl := range got {
names = append(names, tbl.Name)
}
if !eqStrings(names, tt.want) {
t.Errorf("got %v, want %v", names, tt.want)
}
byPattern := FilterTablesByPattern(tt.in, tt.pattern)
if len(byPattern) != len(got) {
t.Errorf("FilterTablesByPattern differs from FilterTables")
}
})
}
}
func TestFilterColumns(t *testing.T) {
cols := testColumns()
tests := []struct {
pattern string
want []string
}{
{"", []string{"created_at", "email", "id", "name", "user_id"}},
{"*_id", []string{"user_id"}},
{"*", []string{"created_at", "email", "id", "name", "user_id"}},
{"nomatch", []string{}},
}
for _, tt := range tests {
if got := colNames(FilterColumns(cols, tt.pattern)); !eqStrings(got, tt.want) {
t.Errorf("pattern %q: got %v, want %v", tt.pattern, got, tt.want)
}
}
if got := FilterColumns(nil, "*"); len(got) != 0 {
t.Errorf("nil map must yield empty result")
}
}
func TestFilterColumnsByType(t *testing.T) {
cols := testColumns()
if got := colNames(FilterColumnsByType(cols, "varchar")); !eqStrings(got, []string{"email", "name"}) {
t.Errorf("varchar: got %v", got)
}
if got := colNames(FilterColumnsByType(cols, "varchar(10)")); !eqStrings(got, []string{"email", "name"}) {
t.Errorf("varchar(10) must match on base type, got %v", got)
}
if got := FilterColumnsByType(cols, "jsonb"); len(got) != 0 {
t.Errorf("jsonb: expected none, got %v", colNames(got))
}
}
func TestFilterColumnFlags(t *testing.T) {
cols := testColumns()
if got := colNames(FilterPrimaryKeys(cols)); !eqStrings(got, []string{"id"}) {
t.Errorf("pks: %v", got)
}
if got := colNames(FilterNullable(cols)); !eqStrings(got, []string{"created_at", "name"}) {
t.Errorf("nullable: %v", got)
}
if got := colNames(FilterNotNull(cols)); !eqStrings(got, []string{"email", "id", "user_id"}) {
t.Errorf("notnull: %v", got)
}
for _, f := range []func(map[string]*models.Column) []*models.Column{FilterPrimaryKeys, FilterNullable, FilterNotNull} {
if got := f(nil); got == nil || len(got) != 0 {
t.Errorf("nil map must give non-nil empty slice")
}
}
}
func TestFilterConstraints(t *testing.T) {
cons := map[string]*models.Constraint{
"pk": {Name: "pk", Type: models.PrimaryKeyConstraint},
"fk": {Name: "fk", Type: models.ForeignKeyConstraint},
"u1": {Name: "u1", Type: models.UniqueConstraint},
"u2": {Name: "u2", Type: models.UniqueConstraint},
"ck": {Name: "ck", Type: models.CheckConstraint},
}
count := func(f func(map[string]*models.Constraint) []*models.Constraint) int { return len(f(cons)) }
if n := count(FilterForeignKeys); n != 1 {
t.Errorf("fk count %d", n)
}
if n := count(FilterUniqueConstraints); n != 2 {
t.Errorf("unique count %d", n)
}
if n := count(FilterCheckConstraints); n != 1 {
t.Errorf("check count %d", n)
}
for _, f := range []func(map[string]*models.Constraint) []*models.Constraint{FilterForeignKeys, FilterUniqueConstraints, FilterCheckConstraints} {
if got := f(nil); got == nil || len(got) != 0 {
t.Errorf("nil map must give non-nil empty slice")
}
}
}
func TestMatchPattern(t *testing.T) {
tests := []struct {
s, pattern string
want bool
}{
{"user_profile", "user_*", true},
{"user", "user_*", false},
{"ab", "a?", true},
{"abc", "a?", false},
{"[a", "[A", true}, // invalid glob: case-insensitive exact
{"x", "[a", false},
}
for _, tt := range tests {
if got := matchPattern(tt.s, tt.pattern); got != tt.want {
t.Errorf("matchPattern(%q,%q) = %v, want %v", tt.s, tt.pattern, got, tt.want)
}
}
}
+118
View File
@@ -0,0 +1,118 @@
package template
import (
"math"
"strings"
"testing"
)
func TestToJSON(t *testing.T) {
if got := ToJSON(map[string]int{"a": 1}); got != `{"a":1}` {
t.Errorf("got %q", got)
}
if got := ToJSON(nil); got != "null" {
t.Errorf("nil: %q", got)
}
if got := ToJSON(math.Inf(1)); !strings.HasPrefix(got, `{"error": "failed to marshal`) {
t.Errorf("marshal failure: %q", got)
}
}
func TestToJSONPretty(t *testing.T) {
got := ToJSONPretty(map[string]int{"a": 1}, " ")
if got != "{\n \"a\": 1\n}" {
t.Errorf("got %q", got)
}
if got := ToJSONPretty(make(chan int), " "); !strings.HasPrefix(got, `{"error"`) {
t.Errorf("marshal failure: %q", got)
}
}
func TestToYAML(t *testing.T) {
if got := ToYAML(map[string]int{"a": 1}); got != "a: 1\n" {
t.Errorf("got %q", got)
}
if got := ToYAML(make(chan int)); !strings.HasPrefix(got, "error: failed to marshal") {
// yaml.v3 panics-recovers into an error for unsupported types
t.Errorf("marshal failure: %q", got)
}
}
func TestIndent(t *testing.T) {
tests := []struct {
in string
spaces int
want string
}{
{"", 4, ""},
{"a", 2, " a"},
{"a\nb", 2, " a\n b"},
{"a\n\nb", 2, " a\n\n b"},
{"a", 0, "a"},
}
for _, tt := range tests {
if got := Indent(tt.in, tt.spaces); got != tt.want {
t.Errorf("Indent(%q,%d) = %q, want %q", tt.in, tt.spaces, got, tt.want)
}
}
if got := IndentWith("", ">"); got != "" {
t.Errorf("IndentWith empty: %q", got)
}
if got := IndentWith("a\n\nb", "> "); got != "> a\n\n> b" {
t.Errorf("IndentWith: %q", got)
}
}
func TestEscape(t *testing.T) {
if got := Escape("a\"b\\c\nd\re\tf"); got != `a\"b\\c\nd\re\tf` {
t.Errorf("got %q", got)
}
if got := Escape(""); got != "" {
t.Errorf("empty: %q", got)
}
if got := EscapeQuotes(`a"b'c`); got != `a\"b\'c` {
t.Errorf("EscapeQuotes: %q", got)
}
}
func TestComment(t *testing.T) {
tests := []struct {
name, in, style, want string
}{
{"empty", "", "//", ""},
{"slashes", "a\nb", "//", "// a\n// b"},
{"hash", "a", "#", "# a"},
{"sql", "a\nb", "--", "-- a\n-- b"},
{"block single", "a", "/* */", "/* a */"},
{"block single alt", "a", "/**/", "/* a */"},
{"block multi", "a\nb", "/* */", "/*\n * a\n * b\n */"},
{"default", "a", "weird", "// a"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Comment(tt.in, tt.style); got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
})
}
}
func TestQuoteUnquote(t *testing.T) {
if got := QuoteString("a"); got != `"a"` {
t.Errorf("QuoteString: %q", got)
}
tests := []struct{ in, want string }{
{`"a"`, "a"},
{`'a'`, "a"},
{`""`, ""},
{`"a'`, `"a'`},
{`a`, `a`},
{`"`, `"`},
{"", ""},
}
for _, tt := range tests {
if got := UnquoteString(tt.in); got != tt.want {
t.Errorf("UnquoteString(%q) = %q, want %q", tt.in, got, tt.want)
}
}
}
+75
View File
@@ -0,0 +1,75 @@
package template
import (
"bytes"
"reflect"
"testing"
"text/template"
)
func TestBuildFuncMapEntriesAreFunctions(t *testing.T) {
fm := BuildFuncMap()
if len(fm) < 100 {
t.Errorf("unexpectedly small func map: %d", len(fm))
}
for name, fn := range fm {
if reflect.TypeOf(fn).Kind() != reflect.Func {
t.Errorf("%s is not a function", name)
}
}
for _, name := range []string{"toSnakeCase", "sqlToGo", "filterTables", "toJSON", "enumerate", "get", "sortTablesByName", "dict", "seq"} {
if _, ok := fm[name]; !ok {
t.Errorf("missing %s", name)
}
}
// Must be accepted by text/template (valid names and signatures).
if _, err := template.New("x").Funcs(fm).Parse("ok"); err != nil {
t.Fatalf("funcmap rejected by text/template: %v", err)
}
}
func TestBuildFuncMapRender(t *testing.T) {
tests := []struct {
name, tmpl, want string
}{
{"add", `{{add 2 3}}`, "5"},
{"sub", `{{sub 5 3}}`, "2"},
{"mul", `{{mul 2 3}}`, "6"},
{"div", `{{div 6 3}}`, "2"},
{"div zero", `{{div 6 0}}`, "0"},
{"mod", `{{mod 7 3}}`, "1"},
{"mod zero", `{{mod 7 0}}`, "0"},
{"default nil", `{{default "d" .Missing}}`, "d"},
{"default set", `{{default "d" "v"}}`, "v"},
{"dict", `{{get (dict "a" 1) "a"}}`, "1"},
{"dict odd", `{{if dict "a"}}set{{else}}nil{{end}}`, "nil"},
{"dict non-string key", `{{if dict 1 2}}set{{else}}nil{{end}}`, "nil"},
{"list", `{{len (list 1 2 3)}}`, "3"},
{"seq", `{{range seq 1 3}}{{.}}{{end}}`, "123"},
{"seq reversed", `{{len (seq 3 1)}}`, "0"},
{"snake", `{{toSnakeCase "UserName"}}`, "user_name"},
{"pluralize", `{{pluralize "category"}}`, "categories"},
{"sqlToGo", `{{sqlToGo "integer" true}}`, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tpl, err := template.New("t").Funcs(BuildFuncMap()).Parse(tt.tmpl)
if err != nil {
t.Fatalf("parse: %v", err)
}
var buf bytes.Buffer
if err := tpl.Execute(&buf, map[string]interface{}{}); err != nil {
t.Fatalf("execute: %v", err)
}
if tt.name == "sqlToGo" {
if buf.Len() == 0 {
t.Error("sqlToGo rendered nothing")
}
return
}
if buf.String() != tt.want {
t.Errorf("got %q, want %q", buf.String(), tt.want)
}
})
}
}
+142
View File
@@ -0,0 +1,142 @@
package template
import (
"reflect"
"testing"
)
type loopItem struct {
Name string
Group string
N int
}
func ints(vs ...interface{}) []interface{} { return vs }
func TestEnumerate(t *testing.T) {
got := Enumerate([]string{"a", "b"})
want := []EnumeratedItem{{0, "a"}, {1, "b"}}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v", got)
}
if got := Enumerate([2]int{5, 6}); len(got) != 2 || got[1].Value != 6 {
t.Errorf("array: %v", got)
}
if got := Enumerate("nope"); len(got) != 0 {
t.Errorf("non-slice: %v", got)
}
if got := Enumerate(nil); len(got) != 0 {
t.Errorf("nil: %v", got)
}
if got := Enumerate([]int{}); len(got) != 0 {
t.Errorf("empty: %v", got)
}
}
func TestBatchChunk(t *testing.T) {
in := []int{1, 2, 3, 4, 5}
got := Batch(in, 2)
want := [][]interface{}{{1, 2}, {3, 4}, {5}}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v", got)
}
if got := Chunk(in, 10); len(got) != 1 || len(got[0]) != 5 {
t.Errorf("size > len: %v", got)
}
for _, size := range []int{0, -1} {
if got := Batch(in, size); len(got) != 0 {
t.Errorf("size %d: %v", size, got)
}
}
if got := Batch([]int{}, 2); len(got) != 0 {
t.Errorf("empty: %v", got)
}
if got := Batch("x", 2); len(got) != 0 {
t.Errorf("non-slice: %v", got)
}
}
func TestReverseFirstLastSkipTake(t *testing.T) {
in := []int{1, 2, 3, 4}
tests := []struct {
name string
got []interface{}
want []interface{}
}{
{"reverse", Reverse(in), ints(4, 3, 2, 1)},
{"reverse empty", Reverse([]int{}), ints()},
{"reverse non-slice", Reverse(5), ints()},
{"first 2", First(in, 2), ints(1, 2)},
{"first n>len", First(in, 9), ints(1, 2, 3, 4)},
{"first 0", First(in, 0), ints()},
{"first non-slice", First(5, 1), ints()},
{"last 2", Last(in, 2), ints(3, 4)},
{"last n>len", Last(in, 9), ints(1, 2, 3, 4)},
{"last neg", Last(in, -1), ints()},
{"last non-slice", Last(5, 1), ints()},
{"skip 1", Skip(in, 1), ints(2, 3, 4)},
{"skip neg", Skip(in, -3), ints(1, 2, 3, 4)},
{"skip all", Skip(in, 4), ints()},
{"skip n>len", Skip(in, 10), ints()},
{"skip non-slice", Skip(5, 1), ints()},
{"take", Take(in, 3), ints(1, 2, 3)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if len(tt.got) != len(tt.want) || (len(tt.want) > 0 && !reflect.DeepEqual(tt.got, tt.want)) {
t.Errorf("got %v, want %v", tt.got, tt.want)
}
})
}
}
func TestConcatUnique(t *testing.T) {
got := Concat([]int{1, 2}, []string{"a"}, 5, nil, [1]int{9})
if !reflect.DeepEqual(got, ints(1, 2, "a", 9)) {
t.Errorf("concat: %v", got)
}
if got := Concat(); len(got) != 0 {
t.Errorf("concat none: %v", got)
}
if got := Unique([]int{1, 2, 1, 3, 2}); !reflect.DeepEqual(got, ints(1, 2, 3)) {
t.Errorf("unique: %v", got)
}
if got := Unique("x"); len(got) != 0 {
t.Errorf("unique non-slice: %v", got)
}
}
func TestSortByGroupByCountIf(t *testing.T) {
items := []loopItem{{"c", "x", 3}, {"a", "y", 1}, {"b", "x", 2}}
sorted := SortBy(items, "Name")
if sorted[0].(loopItem).Name != "a" || sorted[2].(loopItem).Name != "c" {
t.Errorf("sortBy Name: %v", sorted)
}
sorted = SortBy(items, "N")
if sorted[0].(loopItem).N != 1 || sorted[2].(loopItem).N != 3 {
t.Errorf("sortBy N: %v", sorted)
}
if items[0].Name != "c" {
t.Errorf("SortBy must not mutate input")
}
if got := SortBy(5, "Name"); len(got) != 0 {
t.Errorf("sortBy non-slice")
}
groups := GroupBy(items, "Group")
if len(groups) != 2 || len(groups["x"]) != 2 || len(groups["y"]) != 1 {
t.Errorf("groupBy: %v", groups)
}
if got := GroupBy(5, "Group"); len(got) != 0 {
t.Errorf("groupBy non-slice")
}
n := CountIf(items, func(v interface{}) bool { return v.(loopItem).Group == "x" })
if n != 2 {
t.Errorf("countIf: %d", n)
}
if got := CountIf(5, func(interface{}) bool { return true }); got != 0 {
t.Errorf("countIf non-slice: %d", got)
}
}
+216
View File
@@ -0,0 +1,216 @@
package template
import (
"reflect"
"testing"
)
type accessItem struct {
Name string
ID int
}
func TestGetAndGetOr(t *testing.T) {
m := map[string]interface{}{"a": 1, "nilv": nil}
if got := Get(m, "a"); got != 1 {
t.Errorf("Get: %v", got)
}
if got := Get(m, "missing"); got != nil {
t.Errorf("Get missing: %v", got)
}
if got := Get(nil, "a"); got != nil {
t.Errorf("Get nil map: %v", got)
}
if got := GetOr(m, "missing", "def"); got != "def" {
t.Errorf("GetOr missing: %v", got)
}
if got := GetOr(m, "nilv", "def"); got != "def" {
t.Errorf("GetOr nil value: %v", got)
}
if got := GetOr(m, "a", "def"); got != 1 {
t.Errorf("GetOr present: %v", got)
}
}
func TestGetPath(t *testing.T) {
cfg := map[string]interface{}{
"db": map[string]interface{}{"conn": map[string]interface{}{"host": "h"}},
}
if got := GetPath(cfg, "db.conn.host"); got != "h" {
t.Errorf("GetPath: %v", got)
}
if got := GetPath(cfg, "db.nope.host"); got != nil {
t.Errorf("GetPath missing: %v", got)
}
if got := GetPathOr(cfg, "db.nope", "dflt"); got != "dflt" {
t.Errorf("GetPathOr: %v", got)
}
if got := GetPathOr(cfg, "db.conn.host", "dflt"); got != "h" {
t.Errorf("GetPathOr present: %v", got)
}
if !HasPath(cfg, "db.conn") || HasPath(cfg, "db.x") || HasPath(nil, "a") {
t.Errorf("HasPath mismatch")
}
}
func TestSafeIndex(t *testing.T) {
s := []string{"a", "b"}
if got := SafeIndex(s, 1); got != "b" {
t.Errorf("SafeIndex: %v", got)
}
for _, i := range []int{-1, 2, 99} {
if got := SafeIndex(s, i); got != nil {
t.Errorf("SafeIndex(%d) must be nil, got %v", i, got)
}
}
if got := SafeIndex("notslice", 0); got != nil {
t.Errorf("non-slice: %v", got)
}
if got := SafeIndexOr(s, 5, "d"); got != "d" {
t.Errorf("SafeIndexOr: %v", got)
}
if got := SafeIndexOr(s, 0, "d"); got != "a" {
t.Errorf("SafeIndexOr present: %v", got)
}
}
func TestHas(t *testing.T) {
m := map[string]int{"a": 1}
var nilPtr *map[string]int
tests := []struct {
name string
m interface{}
key interface{}
want bool
}{
{"present", m, "a", true},
{"missing", m, "b", false},
{"pointer to map", &m, "a", true},
{"nil pointer", nilPtr, "a", false},
{"non-map", []int{1}, 0, false},
{"nil", nil, "a", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Has(tt.m, tt.key); got != tt.want {
t.Errorf("got %v", got)
}
})
}
}
func TestKeysValues(t *testing.T) {
m := map[string]int{"a": 1, "b": 2}
if got := Keys(m); len(got) != 2 {
t.Errorf("Keys: %v", got)
}
if got := Values(m); len(got) != 2 {
t.Errorf("Values: %v", got)
}
if got := Keys(nil); len(got) != 0 {
t.Errorf("Keys nil: %v", got)
}
if got := Values(5); len(got) != 0 {
t.Errorf("Values non-map: %v", got)
}
}
func TestMerge(t *testing.T) {
m1 := map[string]int{"a": 1, "b": 2}
m2 := map[string]int{"b": 3, "c": 4}
var nilPtr *map[string]int
got := Merge(m1, &m2, nilPtr, nil, 5)
want := map[interface{}]interface{}{"a": 1, "b": 3, "c": 4}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v", got)
}
if got := Merge(); len(got) != 0 {
t.Errorf("empty merge: %v", got)
}
}
func TestPickOmit(t *testing.T) {
m := map[string]int{"a": 1, "b": 2, "c": 3}
var nilPtr *map[string]int
if got := Pick(m, "a", "z"); !reflect.DeepEqual(got, map[interface{}]interface{}{"a": 1}) {
t.Errorf("Pick: %v", got)
}
if got := Pick(&m, "b"); len(got) != 1 {
t.Errorf("Pick ptr: %v", got)
}
if got := Pick(nilPtr, "a"); len(got) != 0 {
t.Errorf("Pick nil ptr: %v", got)
}
if got := Pick(5, "a"); len(got) != 0 {
t.Errorf("Pick non-map: %v", got)
}
if got := Omit(m, "a", "z"); !reflect.DeepEqual(got, map[interface{}]interface{}{"b": 2, "c": 3}) {
t.Errorf("Omit: %v", got)
}
if got := Omit(&m); len(got) != 3 {
t.Errorf("Omit ptr: %v", got)
}
if got := Omit(nilPtr, "a"); len(got) != 0 {
t.Errorf("Omit nil ptr: %v", got)
}
if got := Omit("x", "a"); len(got) != 0 {
t.Errorf("Omit non-map: %v", got)
}
}
func TestSliceContainsIndexOf(t *testing.T) {
s := []string{"a", "b", "c"}
sp := &s
var nilPtr *[]string
if !SliceContains(s, "b") || SliceContains(s, "z") {
t.Errorf("SliceContains")
}
if !SliceContains(sp, "c") || !SliceContains([2]int{1, 2}, 2) {
t.Errorf("SliceContains ptr/array")
}
if SliceContains(nilPtr, "a") || SliceContains("str", "s") || SliceContains(nil, 1) {
t.Errorf("SliceContains invalid input")
}
if got := IndexOf(s, "c"); got != 2 {
t.Errorf("IndexOf: %d", got)
}
if got := IndexOf(sp, "a"); got != 0 {
t.Errorf("IndexOf ptr: %d", got)
}
for _, in := range []interface{}{s, nilPtr, "str", nil} {
if got := IndexOf(in, "zzz"); got != -1 {
t.Errorf("IndexOf miss %v: %d", in, got)
}
}
}
func TestPluck(t *testing.T) {
items := []*accessItem{{"a", 1}, nil, {"c", 3}}
got := Pluck(items, "Name")
if !reflect.DeepEqual(got, []interface{}{"a", nil, "c"}) {
t.Errorf("struct ptrs: %v", got)
}
if got := Pluck([]accessItem{{"a", 1}}, "Missing"); !reflect.DeepEqual(got, []interface{}{nil}) {
t.Errorf("missing field: %v", got)
}
maps := []map[string]int{{"k": 1}, {"x": 2}}
if got := Pluck(maps, "k"); !reflect.DeepEqual(got, []interface{}{1, nil}) {
t.Errorf("maps: %v", got)
}
if got := Pluck([]int{1, 2}, "k"); !reflect.DeepEqual(got, []interface{}{nil, nil}) {
t.Errorf("scalars: %v", got)
}
var nilPtr *[]accessItem
if got := Pluck(nilPtr, "Name"); len(got) != 0 {
t.Errorf("nil ptr: %v", got)
}
if got := Pluck("str", "Name"); len(got) != 0 {
t.Errorf("non-slice: %v", got)
}
s := []accessItem{{"z", 9}}
if got := Pluck(&s, "ID"); !reflect.DeepEqual(got, []interface{}{9}) {
t.Errorf("ptr to slice: %v", got)
}
}
+151
View File
@@ -0,0 +1,151 @@
package template
import (
"reflect"
"testing"
)
func TestCaseConversions(t *testing.T) {
tests := []struct {
in, camel, pascal, snake, kebab string
}{
{"", "", "", "", ""},
{"user_name", "userName", "UserName", "user_name", "user-name"},
{"http_request", "httpRequest", "HTTPRequest", "http_request", "http-request"},
{"user_id", "userID", "UserID", "user_id", "user-id"},
{"UserName", "username", "UserName", "user_name", "user-name"},
{"HTTPRequest", "httprequest", "HTTPRequest", "http_request", "http-request"},
{"userID", "userid", "UserID", "user_id", "user-id"},
{"name", "name", "Name", "name", "name"},
{"ÜberUser", "überuser", "ÜberUser", "über_user", "über-user"},
}
for _, tt := range tests {
t.Run(tt.in, func(t *testing.T) {
if got := ToCamelCase(tt.in); got != tt.camel {
t.Errorf("ToCamelCase = %q, want %q", got, tt.camel)
}
if got := ToPascalCase(tt.in); got != tt.pascal {
t.Errorf("ToPascalCase = %q, want %q", got, tt.pascal)
}
if got := ToSnakeCase(tt.in); got != tt.snake {
t.Errorf("ToSnakeCase = %q, want %q", got, tt.snake)
}
if got := ToKebabCase(tt.in); got != tt.kebab {
t.Errorf("ToKebabCase = %q, want %q", got, tt.kebab)
}
})
}
}
func TestPluralize(t *testing.T) {
tests := []struct{ in, want string }{
{"", ""},
{"user", "users"},
{"person", "people"},
{"Person", "people"},
{"status", "statuses"},
{"cats", "cats"},
{"bus", "buses"},
{"dress", "dresses"},
{"box", "boxes"},
{"quiz", "quizes"},
{"church", "churches"},
{"dish", "dishes"},
{"category", "categories"},
{"day", "days"},
{"leaf", "leaves"},
{"knife", "knives"},
{"hero", "heroes"},
{"video", "videos"},
}
for _, tt := range tests {
if got := Pluralize(tt.in); got != tt.want {
t.Errorf("Pluralize(%q) = %q, want %q", tt.in, got, tt.want)
}
}
}
func TestSingularize(t *testing.T) {
tests := []struct{ in, want string }{
{"", ""},
{"users", "user"},
{"people", "person"},
{"Children", "child"},
{"categories", "category"},
{"ies", "ie"},
{"leaves", "leaf"},
{"buses", "bus"},
{"boxes", "box"},
{"churches", "church"},
{"dishes", "dish"},
{"dress", "dress"},
{"user", "user"},
}
for _, tt := range tests {
if got := Singularize(tt.in); got != tt.want {
t.Errorf("Singularize(%q) = %q, want %q", tt.in, got, tt.want)
}
}
}
func TestPlainStringWrappers(t *testing.T) {
if ToUpper("aB") != "AB" || ToLower("aB") != "ab" {
t.Error("case")
}
if Title("hello world") != "Hello World" || Title("") != "" {
t.Errorf("Title: %q", Title("hello world"))
}
if Trim(" a \n") != "a" {
t.Error("Trim")
}
if TrimPrefix("foobar", "foo") != "bar" || TrimPrefix("bar", "foo") != "bar" {
t.Error("TrimPrefix")
}
if TrimSuffix("foobar", "bar") != "foo" || TrimSuffix("foo", "bar") != "foo" {
t.Error("TrimSuffix")
}
if Replace("aaa", "a", "b", 2) != "bba" || Replace("aaa", "a", "b", -1) != "bbb" {
t.Error("Replace")
}
if !StringContains("abc", "b") || StringContains("abc", "z") {
t.Error("StringContains")
}
if !HasPrefix("abc", "ab") || HasPrefix("abc", "bc") {
t.Error("HasPrefix")
}
if !HasSuffix("abc", "bc") || HasSuffix("abc", "ab") {
t.Error("HasSuffix")
}
if got := Split("a,b", ","); !reflect.DeepEqual(got, []string{"a", "b"}) {
t.Errorf("Split: %v", got)
}
if Join([]string{"a", "b"}, "-") != "a-b" || Join(nil, "-") != "" {
t.Error("Join")
}
}
func TestCapitalizeAndIsVowel(t *testing.T) {
tests := []struct{ in, want string }{
{"", ""},
{"id", "ID"},
{"Uuid", "UUID"},
{"http", "HTTP"},
{"name", "Name"},
{"élan", "Élan"},
}
for _, tt := range tests {
if got := capitalize(tt.in); got != tt.want {
t.Errorf("capitalize(%q) = %q, want %q", tt.in, got, tt.want)
}
}
for _, c := range []byte("aeiouAEIOU") {
if !isVowel(c) {
t.Errorf("%c should be vowel", c)
}
}
for _, c := range []byte("bcxyz") {
if isVowel(c) {
t.Errorf("%c should not be vowel", c)
}
}
}
@@ -0,0 +1,86 @@
package template
import (
"testing"
"git.warky.dev/wdevs/relspecgo/pkg/models"
)
func sampleDB() (*models.Database, *models.Schema, *models.Table) {
db := models.InitDatabase("shop")
schema := models.InitSchema("public")
table := models.InitTable("users", "public")
col := models.InitColumn("id", "users", "public")
col.Type = "integer"
col.IsPrimaryKey = true
table.Columns["id"] = col
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
return db, schema, table
}
func TestTemplateDataConstructors(t *testing.T) {
db, schema, table := sampleDB()
meta := map[string]interface{}{"k": "v"}
dd := NewDatabaseData(db, meta)
if dd.Database != db || dd.ParentDatabase != db || dd.Summary == nil || len(dd.FlatColumns) != 1 || len(dd.FlatTables) != 1 || dd.Metadata["k"] != "v" {
t.Errorf("database data: %+v", dd)
}
if dd.Name() != "shop" {
t.Errorf("name: %q", dd.Name())
}
sd := NewSchemaData(schema, meta)
if sd.Schema != schema || sd.ParentDatabase == nil || sd.ParentDatabase.Name != "public" || len(sd.FlatColumns) != 1 {
t.Errorf("schema data: %+v", sd)
}
if sd.Name() != "public" {
t.Errorf("name: %q", sd.Name())
}
td := NewTableData(table, schema, db, meta)
if td.Table != table || td.ParentSchema != schema || td.ParentDatabase != db || td.Name() != "users" {
t.Errorf("table data: %+v", td)
}
dom := &models.Domain{Name: "billing"}
dmd := NewDomainData(dom, db, meta)
if dmd.Domain != dom || dmd.ParentDatabase != db || dmd.Name() != "billing" {
t.Errorf("domain data: %+v", dmd)
}
sc := &models.Script{Name: "seed"}
scd := NewScriptData(sc, schema, db, meta)
if scd.Script != sc || scd.ParentSchema != schema || scd.Name() != "seed" {
t.Errorf("script data: %+v", scd)
}
if got := (&TemplateData{}).Name(); got != "output" {
t.Errorf("empty name: %q", got)
}
}
func TestTypeMappersDelegate(t *testing.T) {
if got := SQLToGo("integer", false); got == "" {
t.Error("SQLToGo")
}
if got := SQLToTypeScript("integer", false); got == "" {
t.Error("SQLToTypeScript")
}
if got := SQLToJava("integer", false); got == "" {
t.Error("SQLToJava")
}
if got := SQLToPython("integer"); got == "" {
t.Error("SQLToPython")
}
if got := SQLToRust("integer", false); got == "" {
t.Error("SQLToRust")
}
if got := SQLToCSharp("integer", false); got == "" {
t.Error("SQLToCSharp")
}
if got := SQLToPhp("integer", false); got == "" {
t.Error("SQLToPhp")
}
}
+219
View File
@@ -0,0 +1,219 @@
package template
import (
"errors"
"os"
"path/filepath"
"strings"
"testing"
"git.warky.dev/wdevs/relspecgo/pkg/models"
"git.warky.dev/wdevs/relspecgo/pkg/writers"
)
func writeTemplateFile(t *testing.T, body string) string {
t.Helper()
p := filepath.Join(t.TempDir(), "t.tmpl")
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
return p
}
func modeDB() *models.Database {
db := models.InitDatabase("shop")
for _, sn := range []string{"a", "b"} {
s := models.InitSchema(sn)
for _, tn := range []string{"t1", "t2"} {
s.Tables = append(s.Tables, models.InitTable(tn, sn))
}
s.Scripts = append(s.Scripts, &models.Script{Name: "seed_" + sn})
db.Schemas = append(db.Schemas, s)
}
db.Domains = append(db.Domains, &models.Domain{Name: "billing"})
return db
}
func newTestWriter(t *testing.T, body, mode, pattern, out string) (*Writer, error) {
t.Helper()
meta := map[string]interface{}{"template_path": writeTemplateFile(t, body)}
if mode != "" {
meta["mode"] = mode
}
if pattern != "" {
meta["filename_pattern"] = pattern
}
return NewWriter(&writers.WriterOptions{OutputPath: out, Metadata: meta})
}
func TestNewWriterErrors(t *testing.T) {
if _, err := NewWriter(&writers.WriterOptions{}); err == nil {
t.Error("expected error for missing template path")
}
_, err := NewWriter(&writers.WriterOptions{Metadata: map[string]interface{}{"template_path": "/no/such/file"}})
var te *TemplateError
if !errors.As(err, &te) || te.Phase != "load" {
t.Errorf("load error: %v", err)
}
_, err = newTestWriter(t, "{{ .Unclosed ", "", "", "")
if !errors.As(err, &te) || te.Phase != "parse" {
t.Errorf("parse error: %v", err)
}
}
func TestWriterModes(t *testing.T) {
tests := []struct {
name, mode, body, pattern string
wantFiles []string
}{
{"database", "database", "{{.Database.Name}}", "", []string{"out.txt"}},
{"schema", "schema", "{{.Schema.Name}}", "{{.Name}}.txt", []string{"a.txt", "b.txt"}},
{"table", "table", "{{.Table.Name}}", "{{.ParentSchema.Name}}_{{.Name}}.txt", []string{"a_t1.txt", "a_t2.txt", "b_t1.txt", "b_t2.txt"}},
{"script", "script", "{{.Script.Name}}", "{{.Name}}.sql", []string{"seed_a.sql", "seed_b.sql"}},
{"domain", "domain", "{{.Domain.Name}}", "{{.Name}}.md", []string{"billing.md"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
outDir := t.TempDir()
out := outDir
if tt.mode == "database" {
out = filepath.Join(outDir, "out.txt")
}
w, err := newTestWriter(t, tt.body, tt.mode, tt.pattern, out)
if err != nil {
t.Fatal(err)
}
if err := w.WriteDatabase(modeDB()); err != nil {
t.Fatal(err)
}
for _, f := range tt.wantFiles {
if _, err := os.Stat(filepath.Join(outDir, f)); err != nil {
t.Errorf("missing %s: %v", f, err)
}
}
entries, _ := os.ReadDir(outDir)
if len(entries) != len(tt.wantFiles) {
t.Errorf("got %d files, want %d", len(entries), len(tt.wantFiles))
}
})
}
}
func TestWriterDatabaseModeContent(t *testing.T) {
out := filepath.Join(t.TempDir(), "sub", "dir", "o.txt")
w, err := newTestWriter(t, "{{.Database.Name}}:{{len .Database.Schemas}}", "", "", out)
if err != nil {
t.Fatal(err)
}
if err := w.WriteDatabase(modeDB()); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(out)
if err != nil || string(data) != "shop:2" {
t.Errorf("content %q err %v", data, err)
}
}
func TestWriterUnknownMode(t *testing.T) {
w, err := newTestWriter(t, "x", "bogus", "", "")
if err != nil {
t.Fatal(err)
}
if err := w.WriteDatabase(modeDB()); err == nil || !strings.Contains(err.Error(), "unknown entrypoint mode") {
t.Errorf("got %v", err)
}
}
func TestWriterExecuteErrors(t *testing.T) {
// Execution failure: field does not exist on TemplateData.
for _, mode := range []string{"database", "schema", "table", "script", "domain"} {
t.Run(mode, func(t *testing.T) {
w, err := newTestWriter(t, "{{.NoSuchField}}", mode, "", t.TempDir())
if err != nil {
t.Fatal(err)
}
err = w.WriteDatabase(modeDB())
var te *TemplateError
if !errors.As(err, &te) || te.Phase != "execute" {
t.Errorf("got %v", err)
}
})
}
}
func TestWriterBadFilenamePattern(t *testing.T) {
for _, pattern := range []string{"{{.Unclosed", "{{.NoSuchField}}"} {
for _, mode := range []string{"schema", "table", "script", "domain"} {
w, err := newTestWriter(t, "x", mode, pattern, t.TempDir())
if err != nil {
t.Fatal(err)
}
if err := w.WriteDatabase(modeDB()); err == nil {
t.Errorf("mode %s pattern %q: expected error", mode, pattern)
}
}
}
}
func TestWriterWriteOutputFailure(t *testing.T) {
// Output path whose parent is a regular file cannot be created.
blocker := filepath.Join(t.TempDir(), "file")
if err := os.WriteFile(blocker, nil, 0o644); err != nil {
t.Fatal(err)
}
w, err := newTestWriter(t, "x", "database", "", filepath.Join(blocker, "child", "o.txt"))
if err != nil {
t.Fatal(err)
}
if err := w.WriteDatabase(modeDB()); err == nil {
t.Error("expected write failure")
}
}
func TestWriterGenerateFilenameOutputPathForms(t *testing.T) {
dir := t.TempDir()
data := NewTableData(models.InitTable("users", "public"), nil, nil, nil)
tests := []struct {
name, out, want string
}{
{"no output path", "", "users.txt"},
{"existing dir", dir, filepath.Join(dir, "users.txt")},
{"trailing separator", filepath.Join(dir, "new") + string(filepath.Separator), filepath.Join(dir, "new", "users.txt")},
{"file path uses its dir", filepath.Join(dir, "x.out"), filepath.Join(dir, "users.txt")},
{"bare file name", "x.out", "users.txt"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w, err := newTestWriter(t, "x", "table", "{{.Name}}.txt", tt.out)
if err != nil {
t.Fatal(err)
}
got, err := w.generateFilename(data)
if err != nil || got != tt.want {
t.Errorf("got %q err %v, want %q", got, err, tt.want)
}
})
}
}
func TestWriterWriteSchemaAndTable(t *testing.T) {
out := filepath.Join(t.TempDir(), "o.txt")
w, err := newTestWriter(t, "{{range .Database.Schemas}}{{.Name}}:{{len .Tables}};{{end}}", "", "", out)
if err != nil {
t.Fatal(err)
}
db := modeDB()
if err := w.WriteSchema(db.Schemas[0]); err != nil {
t.Fatal(err)
}
if data, _ := os.ReadFile(out); string(data) != "a:2;" {
t.Errorf("WriteSchema: %q", data)
}
if err := w.WriteTable(db.Schemas[1].Tables[0]); err != nil {
t.Fatal(err)
}
if data, _ := os.ReadFile(out); string(data) != "b:1;" {
t.Errorf("WriteTable: %q", data)
}
}