SQL: Add sql template test helper (#91953)

This commit is contained in:
Ryan McKinley
2024-08-16 14:36:56 +03:00
committed by GitHub
parent ac72098248
commit d9cabe5e14
131 changed files with 1377 additions and 911 deletions
@@ -0,0 +1,147 @@
package mocks
import (
"fmt"
"os"
"path/filepath"
reflect "reflect"
"strings"
"testing"
"text/template"
"github.com/google/go-cmp/cmp"
sqltemplate "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
"github.com/stretchr/testify/require"
)
func NewTestingSQLTemplate() sqltemplate.SQLTemplateIface {
standard := sqltemplate.New(sqltemplate.MySQL) // dialect gets replaced at each iteration
return &testingSQLTemplate{standard}
}
type testingSQLTemplate struct {
*sqltemplate.SQLTemplate
}
func (t *testingSQLTemplate) Arg(x any) string {
_ = t.SQLTemplate.Arg(x) // discard the output
switch v := reflect.ValueOf(x); {
case v.Kind() == reflect.Bool:
if v.Bool() {
return "TRUE"
}
return "FALSE"
case v.CanInt(), v.CanUint(), v.CanFloat():
_, ok := x.(fmt.Stringer)
if !ok {
return fmt.Sprintf("%v", x)
}
}
return fmt.Sprintf("'%v'", x) // single quotes
}
func (t *testingSQLTemplate) ArgList(slice reflect.Value) (string, error) {
// Copied from upstream Arg
if !slice.IsValid() || slice.Kind() != reflect.Slice {
return "", sqltemplate.ErrInvalidArgList
}
sliceLen := slice.Len()
if sliceLen == 0 {
return "", nil
}
var b strings.Builder
b.Grow(3*sliceLen - 2) // the list will be ?, ?, ?
for i, l := 0, slice.Len(); i < l; i++ {
if i > 0 {
b.WriteString(", ")
}
b.WriteString(t.Arg(slice.Index(i).Interface()))
}
return b.String(), nil
}
type TemplateTestCase struct {
Name string
// Data should be the struct passed to the template.
Data sqltemplate.SQLTemplateIface
}
type TemplateTestSetup struct {
// Where the snapshots can be found
RootDir string
// The template will be run through each dialect
Dialects []sqltemplate.Dialect
// Check a set of templates against example inputs
Templates map[*template.Template][]TemplateTestCase
}
func CheckQuerySnapshots(t *testing.T, setup TemplateTestSetup) {
t.Helper()
t.Parallel()
if len(setup.Dialects) < 1 {
setup.Dialects = []sqltemplate.Dialect{
sqltemplate.MySQL,
sqltemplate.SQLite,
sqltemplate.PostgreSQL,
}
}
for tmpl, cases := range setup.Templates {
t.Run(tmpl.Name(), func(t *testing.T) {
t.Parallel()
tname := strings.TrimSuffix(tmpl.Name(), ".sql")
for _, input := range cases {
t.Run(input.Name, func(t *testing.T) {
t.Parallel()
require.NotPanics(t, func() {
for _, dialect := range setup.Dialects {
t.Run(dialect.DialectName(), func(t *testing.T) {
// not parallel because we're sharing tc.Data,
// but also not worth deep cloning
input.Data.SetDialect(dialect)
err := input.Data.Validate()
require.NoError(t, err)
got, err := sqltemplate.Execute(tmpl, input.Data)
require.NoError(t, err)
clean := sqltemplate.RemoveEmptyLines(got)
update := false
fname := fmt.Sprintf("%s--%s-%s.sql", dialect.DialectName(), tname, input.Name)
fpath := filepath.Join(setup.RootDir, fname)
// We can ignore the gosec G304 because this is only for tests
// nolint:gosec
expect, err := os.ReadFile(fpath)
if err != nil || len(expect) < 1 {
update = true
t.Errorf("missing " + fpath)
} else {
if diff := cmp.Diff(string(expect), clean); diff != "" {
t.Errorf("%s: %s", fname, diff)
update = true
}
}
if update {
_ = os.WriteFile(fpath, []byte(clean), 0777)
}
})
}
})
})
}
})
}
}
@@ -11,21 +11,25 @@ import (
var (
ErrValidationNotImplemented = errors.New("validation not implemented")
ErrSQLTemplateNoSerialize = errors.New("SQLTemplate should not be serialized")
// Make sure SQLTemplate implements the interface
_ SQLTemplateIface = (*SQLTemplate)(nil)
)
// SQLTemplate provides comprehensive support for SQL templating, handling
// dialect traits, execution arguments and scanning arguments.
type SQLTemplate struct {
Dialect
Args
ScanDest
*Args
*ScanDest
}
// New returns a nee *SQLTemplate that will use the given dialect.
func New(d Dialect) *SQLTemplate {
ret := new(SQLTemplate)
ret.ScanDest = new(ScanDest)
ret.Args = NewArgs(d)
ret.SetDialect(d)
return ret
}