-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathe2e_sqlite_test.go
More file actions
270 lines (252 loc) · 8.39 KB
/
e2e_sqlite_test.go
File metadata and controls
270 lines (252 loc) · 8.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
// Package main provides end-to-end tests for the gormdb2struct tool.
package main
import (
"context"
"database/sql"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
_ "github.com/glebarez/go-sqlite"
)
// TestEndToEndSQLite generates models from a temp SQLite DB and then builds and runs
// a tiny program that uses the generated package to insert, read, and update rows.
func TestEndToEndSQLite(t *testing.T) {
// Skip on short to keep CI fast if needed; this is an E2E test.
if testing.Short() {
t.Skip("skipping e2e in short mode")
}
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "test.db")
// Create sqlite schema with various data types and a relation to test ExtraFields.
dsn := fmt.Sprintf("file:%s?cache=shared&_fk=1", dbPath)
db, err := sql.Open("sqlite", dsn)
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
t.Cleanup(func() { _ = db.Close() })
schema := []string{
// main table covering many type mappings
`CREATE TABLE IF NOT EXISTS all_types (
id INTEGER PRIMARY KEY,
bool_col BOOLEAN,
tiny1 TINYINT(1),
int_col INTEGER NOT NULL DEFAULT 0,
big_col BIGINT,
real_col REAL,
double_col DOUBLE,
float_col FLOAT,
text_col TEXT,
varchar_col VARCHAR(255),
char_col CHAR(10),
blob_col BLOB,
date_col DATE,
datetime_col DATETIME,
ts_col TIMESTAMP,
numeric_col NUMERIC,
decimal_col DECIMAL,
duration_col DURATION,
json_col JSONB
);`,
// second table to exercise relation via ExtraFields (one-to-many)
`CREATE TABLE IF NOT EXISTS child (
id INTEGER PRIMARY KEY,
all_types_id INTEGER NOT NULL,
name TEXT,
FOREIGN KEY(all_types_id) REFERENCES all_types(id)
);`,
}
for _, q := range schema {
if _, err := db.Exec(q); err != nil {
t.Fatalf("create schema: %v for query %s", err, q)
}
}
// Create an OutPath under the project root so imports resolve within the same module.
outPath := filepath.Join(projectRoot(t), "generated_e2e")
if err := os.MkdirAll(outPath, 0o755); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.RemoveAll(outPath) })
// Write TOML config for generator.
cfgToml := fmt.Sprintf(`
OutPath = %q
DatabaseDialect = "sqlite"
GenerateDbInit = true
IncludeAutoMigrate = true
CleanUp = true
Sqlitedbpath = %q
[ExtraFields]
[[ExtraFields."all_types"]]
StructPropName = "Children"
StructPropType = "models.Child"
FkStructPropName = "AllTypesID"
RefStructPropName = "ID"
HasMany = true
Pointer = false
`, outPath, dbPath)
cfgPath := filepath.Join(tmpDir, "config.toml")
if err := os.WriteFile(cfgPath, []byte(cfgToml), 0o644); err != nil {
t.Fatal(err)
}
// Run generator: go run . <config>
cmd := exec.CommandContext(context.Background(), "go", "run", ".", cfgPath)
cmd.Dir = projectRoot(t)
cmd.Env = os.Environ()
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("generator failed: %v\nOutput:\n%s", err, string(out))
}
// Verify expected generated files exist
mustExist(t, filepath.Join(outPath, "models"))
mustExist(t, filepath.Join(outPath, "db_sqlite.go"))
// Patch generated db file import path to full module path and drop slog-gorm to avoid external dep
dbInitPath := filepath.Join(outPath, "db_sqlite.go")
b, err := os.ReadFile(dbInitPath)
if err != nil {
t.Fatal(err)
}
pkgBase := filepath.Base(outPath)
patched := strings.ReplaceAll(string(b), fmt.Sprintf("\"%s/models\"", pkgBase), fmt.Sprintf("\"%s/%s/models\"", modulePath(t), pkgBase))
patched = strings.ReplaceAll(patched, "slogGorm \"github.com/orandin/slog-gorm\"\n\t\"github.com/glebarez/sqlite\"", "\"github.com/glebarez/sqlite\"")
patched = strings.ReplaceAll(patched, "&gorm.Config{Logger: slogGorm.New()}", "&gorm.Config{}")
if err := os.WriteFile(dbInitPath, []byte(patched), 0o644); err != nil {
t.Fatal(err)
}
// Determine the generated struct name for the all_types table by reading its model file
modelsDir := filepath.Join(outPath, "models")
entries, err := os.ReadDir(modelsDir)
if err != nil {
t.Fatal(err)
}
var allTypesFile string
for _, e := range entries {
if strings.HasPrefix(e.Name(), "all_types") && strings.HasSuffix(e.Name(), ".go") {
allTypesFile = filepath.Join(modelsDir, e.Name())
break
}
}
if allTypesFile == "" {
t.Fatalf("could not find all_types model file in %s", modelsDir)
}
mb, err := os.ReadFile(allTypesFile)
if err != nil {
t.Fatal(err)
}
// very simple parse: find `type <Name> struct {`
var modelType string
for _, ln := range strings.Split(string(mb), "\n") {
ln = strings.TrimSpace(ln)
if strings.HasPrefix(ln, "type ") && strings.Contains(ln, " struct {") {
parts := strings.Split(ln, " ")
if len(parts) >= 3 {
modelType = parts[1]
break
}
}
}
if modelType == "" {
t.Fatalf("unable to determine model type name from %s", allTypesFile)
}
// Create a small program under this module that uses the generated package
cmdDir := filepath.Join(projectRoot(t), "cmd_e2e")
if err := os.MkdirAll(cmdDir, 0o755); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.RemoveAll(cmdDir) })
mainGo := fmt.Sprintf(`package main
import (
"fmt"
"time"
"gorm.io/datatypes"
g "%s/%s"
m "%s/%s/models"
)
func main(){
g.DbInit(%q)
// Insert
js := datatypes.JSONMap(map[string]any{"a": 1, "b": 2})
a := &m.%s{BoolCol: ptrBool(true), Tiny1: ptrStr("1"), IntCol: ptrI64(42), BigCol: ptrI64(4200), RealCol: ptrF64(1.5), DoubleCol: ptrF64(2.5), FloatCol: ptrF32(3.5), TextCol: ptrStr("hello"), VarcharCol: ptrStr("v"), CharCol: ptrStr("c"), BlobCol: ptrBytes([]byte{1,2,3}), DateCol: ptrTime(1700000000), DatetimeCol: ptrTime(1700000100), TsCol: ptrTime(1700000200), NumericCol: ptrF64(10.5), DecimalCol: ptrF64(20.5), DurationCol: ptrDur(1234567890), JSONCol: &js}
if err := g.DB.Create(a).Error; err != nil { panic(err) }
// Read
var got m.%s
if err := g.DB.First(&got, a.ID).Error; err != nil { panic(err) }
// Update each field
b := false
jsu := datatypes.JSONMap(map[string]any{"c": 3, "d": 4})
if err := g.DB.Model(&got).Updates(map[string]any{
"bool_col": &b,
"tiny1": ptrStr("0"),
"int_col": ptrI64(43),
"big_col": ptrI64(4300),
"real_col": ptrF64(9.5),
"double_col": ptrF64(8.5),
"float_col": ptrF32(7.5),
"text_col": ptrStr("world"),
"varchar_col": ptrStr("vv"),
"char_col": ptrStr("cc"),
"blob_col": ptrBytes([]byte{9,8,7}),
"date_col": ptrTime(1700001000),
"datetime_col": ptrTime(1700001100),
"ts_col": ptrTime(1700001200),
"numeric_col": ptrF64(11.5),
"decimal_col": ptrF64(21.5),
"duration_col": ptrDur(987654321),
"json_col": &jsu,
}).Error; err != nil { panic(err) }
var after m.%s
if err := g.DB.First(&after, a.ID).Error; err != nil { panic(err) }
if after.TextCol == nil || *after.TextCol != "world" { panic(fmt.Sprintf("unexpected text: %%v", after.TextCol)) }
fmt.Print("OK")
}
func ptrStr(s string)*string{ return &s }
func ptrI64(v int64)*int64{ return &v }
func ptrF64(v float64)*float64{ return &v }
func ptrF32(v float32)*float32{ return &v }
func ptrBool(v bool)*bool{ return &v }
func ptrBytes(b []byte)*[]byte{ return &b }
func ptrTime(sec int64)*time.Time{ t:=time.Unix(sec,0); return &t }
func ptrDur(n int64)*time.Duration{ d:=time.Duration(n); return &d }
`, modulePath(t), pkgBase, modulePath(t), pkgBase, dbPath, modelType, modelType, modelType)
if err := os.WriteFile(filepath.Join(cmdDir, "main.go"), []byte(mainGo), 0o644); err != nil {
t.Fatal(err)
}
// Build and run the small program from the repo root
run := exec.Command("go", "run", "./cmd_e2e")
run.Dir = projectRoot(t)
run.Env = os.Environ()
progOut, err := run.CombinedOutput()
if err != nil {
t.Fatalf("use app failed: %v\nOutput:\n%s", err, string(progOut))
}
if !strings.Contains(string(progOut), "OK") {
t.Fatalf("unexpected output: %s", string(progOut))
}
}
func mustExist(t *testing.T, p string) {
t.Helper()
if _, err := os.Stat(p); err != nil {
t.Fatalf("expected exists: %s: %v", p, err)
}
}
// projectRoot returns the repo root directory (where this test file lives).
func projectRoot(_ *testing.T) string {
_, file, _, _ := runtime.Caller(0)
return filepath.Dir(file)
}
func modulePath(t *testing.T) string {
b, err := os.ReadFile(filepath.Join(projectRoot(t), "go.mod"))
if err != nil {
t.Fatal(err)
}
for _, ln := range strings.Split(string(b), "\n") {
ln = strings.TrimSpace(ln)
if strings.HasPrefix(ln, "module ") {
return strings.TrimSpace(strings.TrimPrefix(ln, "module "))
}
}
t.Fatal("module path not found")
return ""
}