Files
grafana/pkg/storage/unified/sql/sqltemplate/mocks/test_snapshots.go
Matheus Macabu fc9f32a9f6 SQLTemplates: Add helper to ensure all templates have a test-case (#103964)
* SQLTemplates: Add helper to ensure all templates have a test-case associated

* UnifiedStorage: Add missing sql template test case

* LegacyDashboards: Add sql templates fs to test cases for exhaustiveness check

* RBACStore: Add sql templates fs to test cases for exhaustiveness check

* LegacyIAM: Add missing sql template test cases
2025-04-22 11:21:51 +02:00

196 lines
4.5 KiB
Go

package mocks
import (
"fmt"
"io/fs"
"os"
"path/filepath"
reflect "reflect"
"strings"
"testing"
"text/template"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
sqltemplate "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
)
func NewTestingSQLTemplate() sqltemplate.SQLTemplate {
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.SQLTemplate
}
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
// The (embedded) filesystem containing the SQL query templates
// If not nil, a test will be run to ensure all templates in that folder have a test-case
SQLTemplatesFS fs.FS
}
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,
}
}
if setup.SQLTemplatesFS != nil {
ensureAllTemplatesHaveTestCases(t, setup)
}
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.Error("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)
}
})
}
})
})
}
})
}
}
func ensureAllTemplatesHaveTestCases(t *testing.T, setup TemplateTestSetup) {
t.Helper()
// Folder containing SQL query templates
sqlFiles := make([]string, 0, len(setup.Templates))
err := fs.WalkDir(setup.SQLTemplatesFS, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if name := d.Name(); strings.HasSuffix(name, ".sql") {
sqlFiles = append(sqlFiles, name)
}
return nil
})
require.NoError(t, err)
// Makes sure all SQL files in the folder have a test-case
for _, file := range sqlFiles {
found := false
for template := range setup.Templates {
if template.Name() == file {
found = true
break
}
}
assert.True(t, found, "File '%s' does not have a test case", file)
}
}