Files
chaisql/sqltests/sql_test.go
2024-02-18 11:11:37 +04:00

279 lines
5.8 KiB
Go

package sql_test
import (
"bufio"
"io"
"io/fs"
"log"
"os"
"path/filepath"
"strings"
"testing"
"github.com/chaisql/chai"
"github.com/chaisql/chai/internal/testutil"
"github.com/chaisql/chai/internal/testutil/assert"
"github.com/stretchr/testify/require"
)
var logger *log.Logger
func logF(format string, v ...any) {
if logger != nil {
logger.Printf(format, v...)
}
}
func logLn(v ...any) {
if logger != nil {
logger.Println(v...)
}
}
func TestSQL(t *testing.T) {
if testing.Verbose() {
logger = log.New(os.Stderr, "[SQL TESTS] ", 0)
}
err := filepath.Walk(".", func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
if info.Name() == "expr" {
return fs.SkipDir
}
return nil
}
if filepath.Ext(info.Name()) != ".sql" {
return nil
}
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
ts := parse(f, path)
absPath, err := filepath.Abs(path)
if err != nil {
return err
}
t.Run(ts.Filename, func(t *testing.T) {
setup := func(t *testing.T, db *chai.DB) {
t.Helper()
err := db.Exec(ts.Setup)
assert.NoError(t, err)
}
logF("Testing file %q with %d suites\n", absPath, len(ts.Suites))
if len(ts.Suites) > 0 {
for _, suite := range ts.Suites {
t.Run(suite.Name, func(t *testing.T) {
var tests []*test
logLn("- Testing suite:", suite.Name)
for _, tt := range suite.Tests {
if tt.Only {
tests = []*test{tt}
break
}
}
if tests == nil {
tests = suite.Tests
}
logLn("- Running", len(tests), "tests")
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
db, err := chai.Open(":memory:")
assert.NoError(t, err)
defer db.Close()
setup(t, db)
logLn("-- Running test:", test.Name)
// post setup
if suite.PostSetup != "" {
err = db.Exec(suite.PostSetup)
assert.NoError(t, err)
}
if test.Fails {
exec := func() error {
res, err := db.Query(test.Expr)
if err != nil {
return err
}
defer res.Close()
return res.Iterate(func(r *chai.Row) error {
_, err := r.MarshalJSON()
return err
})
}
err := exec()
if test.ErrorMatch != "" {
require.NotNilf(t, err, "%s:%d expected error, got nil", absPath, test.Line)
require.Equal(t, test.ErrorMatch, err.Error(), "Source %s:%d", absPath, test.Line)
} else {
assert.Errorf(t, err, "\nSource:%s:%d expected\n%s\nto raise an error but got none", absPath, test.Line, test.Expr)
}
} else {
res, err := db.Query(test.Expr)
require.NoError(t, err, "Source: %s:%d", absPath, test.Line)
defer res.Close()
testutil.RequireStreamEqf(t, test.Result, res, "Source: %s:%d", absPath, test.Line)
}
})
}
})
}
}
})
return nil
})
assert.NoError(t, err)
}
type test struct {
Name string
Expr string
Result string
ErrorMatch string
Fails bool
Line int
Only bool
}
type suite struct {
Name string
PostSetup string
Tests []*test
}
type testSuite struct {
Filename string
Setup string
Suites []suite
}
func parse(r io.Reader, filename string) *testSuite {
s := bufio.NewScanner(r)
ts := testSuite{
Filename: filename,
}
var curTest *test
var readingResult bool
var readingSetup bool
var readingSuite bool
var readingCommentBlock bool
var suiteIndex int = -1
var only bool
var lineCount = 0
for s.Scan() {
lineCount++
line := s.Text()
// keep result indentation intact
if !readingResult {
line = strings.TrimSpace(line)
}
switch {
case line == "":
// ignore blank lines
case readingCommentBlock && strings.TrimSpace(line) == "*/":
readingCommentBlock = false
case readingCommentBlock:
// ignore comment blocks
case strings.HasPrefix(line, "-- setup:"):
readingSetup = true
case strings.HasPrefix(line, "-- suite:"):
readingSuite = true
suiteIndex++
ts.Suites = append(ts.Suites, suite{
Name: strings.TrimPrefix(line, "-- suite: "),
})
case strings.HasPrefix(line, "-- only:"):
only = true
fallthrough
case strings.HasPrefix(line, "-- test:"):
readingSetup = false
readingSuite = false
// create a new test
name := strings.TrimPrefix(line, "-- test: ")
curTest = &test{
Name: name,
Line: lineCount,
Only: only,
}
only = false
// if there are no suites, create one by default
if suiteIndex == -1 {
suiteIndex++
ts.Suites = append(ts.Suites, suite{
Name: "default",
})
}
// add test to each suite
for i := range ts.Suites {
ts.Suites[i].Tests = append(ts.Suites[i].Tests, curTest)
}
case strings.HasPrefix(line, "/* result:"), strings.HasPrefix(line, "/*result:"):
readingResult = true
case strings.HasPrefix(line, "-- error:"):
error := strings.TrimPrefix(line, "-- error:")
error = strings.TrimSpace(error)
if error == "" {
// handle the case where error was used but without a message
curTest.Fails = true
} else {
curTest.ErrorMatch = error
curTest.Fails = true
}
curTest = nil
case strings.HasPrefix(line, "/*"): // ignore block comments
readingCommentBlock = true
case strings.HasPrefix(line, "--"):
// ignore line comments
case !readingResult && strings.TrimSpace(line) == "*/":
default:
if readingSuite {
ts.Suites[suiteIndex].PostSetup += line + "\n"
} else if readingSetup {
ts.Setup += line + "\n"
} else if readingResult && strings.TrimSpace(line) == "*/" {
readingResult = false
curTest = nil
} else if readingResult {
curTest.Result += line + "\n"
} else {
curTest.Expr += line + "\n"
}
}
}
return &ts
}