mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
* stub out a test framework for fbsql * Introduce fbsql integration test framework. This also adds support for `pset location` to set the geo location (i.e. time zone) in which timestamps should be displayed. And it adds support for comments (lines starting with `--`) in the line reader/splitter. * Replace Stdin and Stderr with setters and un-export them
255 lines
6.3 KiB
Go
255 lines
6.3 KiB
Go
package cli_test
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/featurebasedb/featurebase/v3/cli"
|
|
"github.com/featurebasedb/featurebase/v3/dax/server/test"
|
|
"github.com/featurebasedb/featurebase/v3/logger"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestCLIIntegration(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
t.Run("Stubbed Framework", func(t *testing.T) {
|
|
mc := test.MustRunManagedCommand(t)
|
|
defer mc.Close()
|
|
|
|
addr := mc.Address()
|
|
|
|
capture := newCapture(t)
|
|
|
|
comparer := newComparer(t)
|
|
comparer.run()
|
|
|
|
fbsql := cli.NewCommand(logger.StderrLogger)
|
|
fbsql.SetStdin(capture)
|
|
fbsql.SetStdout(comparer)
|
|
fbsql.SetStderr(comparer)
|
|
|
|
fbsql.Config = &cli.Config{
|
|
Host: addr.Host(),
|
|
Port: fmt.Sprintf("%d", addr.Port()),
|
|
}
|
|
|
|
// Run fbsql in a goroutine so we can continue to send it commands
|
|
// below.
|
|
didQuit := make(chan struct{})
|
|
go func() {
|
|
require.NoError(t, fbsql.Run(ctx))
|
|
close(didQuit)
|
|
}()
|
|
|
|
// testFiles reference files located in the cli/testdata directory. All
|
|
// tests should be placed there; other than adding another test file to
|
|
// this list, you probably shouldn't be editing this file unless you are
|
|
// trying to modify the way the test framework itself works.
|
|
testFiles := []string{
|
|
"setup",
|
|
"database",
|
|
"table",
|
|
// the tests below may be dependent on the previous tests, which do
|
|
// setup and some shared database and table creation.
|
|
"query_buffer",
|
|
// meta commands
|
|
"meta_bang",
|
|
"meta_cd",
|
|
"meta_echo",
|
|
"meta_file",
|
|
"meta_pset_border",
|
|
"meta_pset_expanded",
|
|
"meta_pset_tuples_only",
|
|
"meta_include",
|
|
"meta_output",
|
|
"meta_set",
|
|
"meta_timing",
|
|
"meta_write",
|
|
}
|
|
|
|
for _, testFile := range testFiles {
|
|
t.Run(testFile, func(t *testing.T) {
|
|
f, err := os.Open("testdata/" + testFile)
|
|
require.NoError(t, err)
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
var lineNo int
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
lineNo++
|
|
|
|
// Empty lines and comments (//) are ignored.
|
|
if line == "" {
|
|
continue
|
|
} else if strings.HasPrefix(line, "//") {
|
|
continue
|
|
}
|
|
|
|
parts := strings.SplitN(line, ":", 2)
|
|
|
|
switch parts[0] {
|
|
case "SEND":
|
|
v := ""
|
|
if len(parts) == 2 {
|
|
v = parts[1]
|
|
}
|
|
capture.sendLine(v)
|
|
case "EXPECT":
|
|
v := ""
|
|
if len(parts) == 2 {
|
|
v = parts[1]
|
|
}
|
|
comparer.expectLine(v, testFile, lineNo)
|
|
case "EXPECTCOMP":
|
|
if len(parts) == 2 {
|
|
comps := strings.SplitN(parts[1], ":", 2)
|
|
v := ""
|
|
if len(comps) == 2 {
|
|
v = comps[1]
|
|
}
|
|
comparer.expectLineComp(comparator(comps[0]), v, testFile, lineNo)
|
|
} else {
|
|
t.Errorf("unexpected line: %s[%d]:%s", testFile, lineNo, line)
|
|
}
|
|
default:
|
|
t.Errorf("unexpected line: %s[%d]:%s", testFile, lineNo, line)
|
|
}
|
|
}
|
|
require.NoError(t, scanner.Err())
|
|
})
|
|
}
|
|
|
|
// End with quit to ensure that fbsql closes without error.
|
|
capture.sendLine(`\q`)
|
|
|
|
// Ensure fbsql quits cleanly.
|
|
select {
|
|
case <-didQuit:
|
|
case <-time.After(time.Second):
|
|
t.Fatalf("expected fbsql to quit")
|
|
}
|
|
})
|
|
}
|
|
|
|
// compare is used to compare fbsql output written to its Stdout with expected
|
|
// lines.
|
|
type comparer struct {
|
|
t *testing.T
|
|
out chan byte
|
|
outline chan []byte
|
|
exp chan []byte
|
|
}
|
|
|
|
func newComparer(t *testing.T) *comparer {
|
|
return &comparer{
|
|
t: t,
|
|
out: make(chan byte, 1024),
|
|
outline: make(chan []byte, 128),
|
|
exp: make(chan []byte, 1024),
|
|
}
|
|
}
|
|
|
|
func (c *comparer) run() {
|
|
// Read bytes off output, and for every line (designated by a line feed "\n"),
|
|
// push the line onto the outline channel.
|
|
go func() {
|
|
var line []byte
|
|
for {
|
|
b := <-c.out
|
|
if b == byte('\n') {
|
|
c.outline <- line
|
|
line = []byte{}
|
|
continue
|
|
}
|
|
line = append(line, b)
|
|
}
|
|
}()
|
|
}
|
|
|
|
type comparator string
|
|
|
|
const (
|
|
compEquals = "Equals"
|
|
compHasPrefix = "HasPrefix"
|
|
compWithFormat = "WithFormat"
|
|
)
|
|
|
|
// expectLine is a convenience method which calls expectLineComp with the compEq
|
|
// comparator and the given line.
|
|
func (c *comparer) expectLine(line string, fileName string, lineNo int) {
|
|
c.expectLineComp(compEquals, line, fileName, lineNo)
|
|
}
|
|
|
|
// expectLineComp reads the next line from the outline channel and compares it
|
|
// with the given `line`. A comparator can be provided to inform how the lines
|
|
// should be compared (for example, the compHasPrefix comparator will just
|
|
// compare the beginning part of the outline).
|
|
func (c *comparer) expectLineComp(comp comparator, line string, fileName string, lineNo int) {
|
|
var outline []byte
|
|
select {
|
|
case outline = <-c.outline:
|
|
case <-time.After(10 * time.Second):
|
|
// TODO(tlt): this is 10 seconds to account for the fb_views creation on
|
|
// a local mac. This should really be something like 2 seconds. Put this
|
|
// back to 2 once fb_views issue is addressed.
|
|
c.t.Fatalf("expected output line %s[%d]: >%s<", fileName, lineNo, line)
|
|
}
|
|
|
|
// msg is included in any require which fails.
|
|
msg := []interface{}{"exp: %s[%d], got: >%s<", fileName, lineNo, outline}
|
|
|
|
switch comp {
|
|
case compEquals:
|
|
require.Equal(c.t, []byte(line), outline, msg...)
|
|
case compHasPrefix:
|
|
require.True(c.t, strings.HasPrefix(string(outline), line), msg...)
|
|
case compWithFormat:
|
|
require.True(c.t, compareByteSlices(outline, []byte(line)), msg...)
|
|
default:
|
|
c.t.Fatalf("invalid comparator: %s", comp)
|
|
}
|
|
}
|
|
|
|
func (c *comparer) Write(b []byte) (n int, err error) {
|
|
for i := range b {
|
|
c.out <- b[i]
|
|
}
|
|
return len(b), err
|
|
}
|
|
|
|
// compareByteSlices compares a byte slice s with another byte slice format and
|
|
// returns true if they are the same. It will accept underscore as a
|
|
// single-character wildcard anywhere in slice format.
|
|
func compareByteSlices(s, format []byte) bool {
|
|
// Replace some helpers in format before comparing.
|
|
f := string(format)
|
|
f = strings.ReplaceAll(f, `{uuid}`, `________-____-____-____-____________`)
|
|
f = strings.ReplaceAll(f, `{timestamp}`, `____-__-__T__:__:__Z`)
|
|
format = []byte(f)
|
|
|
|
if len(s) != len(format) {
|
|
return false
|
|
}
|
|
|
|
for i := range s {
|
|
if format[i] == '_' {
|
|
continue
|
|
}
|
|
if s[i] != format[i] {
|
|
// log.Printf("DEBUG: characters differ: (%d): '%v' != '%v'", i, s[i], format[i])
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|