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
199 lines
5.2 KiB
Go
199 lines
5.2 KiB
Go
package cli_test
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
featurebase "github.com/featurebasedb/featurebase/v3"
|
|
"github.com/featurebasedb/featurebase/v3/cli"
|
|
"github.com/featurebasedb/featurebase/v3/logger"
|
|
"github.com/pkg/errors"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestCLI(t *testing.T) {
|
|
t.Run("Input", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
capture := newCapture(t)
|
|
|
|
cli := cli.NewCommand(logger.StderrLogger)
|
|
cli.SetStdin(capture)
|
|
cli.SetStdout(capture)
|
|
cli.Queryer = capture
|
|
|
|
go func() {
|
|
assert.NoError(t, cli.Run(ctx))
|
|
}()
|
|
|
|
none := []string{}
|
|
|
|
// One statement, one line.
|
|
capture.Assert("one;", []string{"one\n"})
|
|
|
|
// One statement, multiple lines.
|
|
capture.Assert("one", none)
|
|
capture.Assert(" two ", none)
|
|
capture.Assert("three;", []string{"one\ntwo\nthree\n"})
|
|
|
|
// Multiple statements, one line.
|
|
capture.Assert("foo; bar;", []string{"foo\n", "bar\n"})
|
|
|
|
// Multiple statements, multiple lines.
|
|
capture.Assert("a1", none)
|
|
capture.Assert("a2; b1", []string{"a1\na2\n"})
|
|
capture.Assert("b2;", []string{"b1\nb2\n"})
|
|
|
|
// Blank lines.
|
|
capture.Assert("one", none)
|
|
capture.Assert("", none)
|
|
capture.Assert("three;", []string{"one\nthree\n"})
|
|
|
|
// Just a semi-colon.
|
|
capture.Assert(";", []string{""})
|
|
|
|
// Multi-line with just a semi-colon.
|
|
capture.Assert("one", none)
|
|
capture.Assert(";", []string{"one\n"})
|
|
|
|
// Ensure a clean exit with no errors.
|
|
assert.NoError(t, capture.Exit())
|
|
})
|
|
}
|
|
|
|
////////////////////////////////////////////////////////
|
|
|
|
// Ensure type implementes interface.
|
|
var _ io.ReadCloser = (*capture)(nil)
|
|
var _ io.Writer = (*capture)(nil)
|
|
var _ cli.Queryer = (*capture)(nil)
|
|
|
|
// capture implements the various CLI interfaces in order to capture test input
|
|
// and submit it as though that input were being read from the command line. It
|
|
// also captures calls made to the Queryer.Query method and ensures the sql they
|
|
// contain is expected.
|
|
type capture struct {
|
|
t *testing.T
|
|
|
|
// ch is a channel of strings (one line at a time) of CLI input.
|
|
ch chan string
|
|
|
|
mu sync.RWMutex
|
|
sqls []string
|
|
|
|
// queryDone will receive an event any time the Query method is called and
|
|
// has completed. This is to tell the Assert method that it's safe to
|
|
// compare the sqls slice.
|
|
queryDone chan struct{}
|
|
|
|
asserting chan struct{}
|
|
|
|
err error
|
|
}
|
|
|
|
func newCapture(t *testing.T) *capture {
|
|
return &capture{
|
|
t: t,
|
|
ch: make(chan string),
|
|
sqls: make([]string, 0),
|
|
queryDone: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
func (c *capture) Exit() error {
|
|
c.sendLine(`\q`)
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.err
|
|
}
|
|
|
|
func (c *capture) Assert(in string, out []string) {
|
|
c.asserting = make(chan struct{})
|
|
|
|
c.sendLine(in)
|
|
|
|
// Wait for the CLI command to complete processing the input and send the
|
|
// sql to Query() by blocking on the queryDone channel. Because Query gets
|
|
// called for every sql statement in the input, an input resulting in
|
|
// multiple sql statements needs to wait for all expected queries to
|
|
// complete. A timeout is included to this so it doesn't deadlock in the
|
|
// case where Query is expected to be called, but isn't; after the timeout,
|
|
// the test should fail completely. In summary: we wait on queryDone the
|
|
// number of sql statements we expect. If we receive fewer than expected,
|
|
// the timeout will occur. If we receive more than expected, the Query()
|
|
// method will effectively deadlock, reach its own timout, then write to
|
|
// capture.err, which will be reported upon Exit().
|
|
for range out {
|
|
select {
|
|
case <-c.queryDone:
|
|
case <-time.After(2 * time.Second):
|
|
c.t.Fatalf("expected Query() to be called")
|
|
}
|
|
}
|
|
|
|
close(c.asserting)
|
|
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
assert.Equal(c.t, out, c.sqls)
|
|
|
|
// Reset the slice.
|
|
c.sqls = c.sqls[:0]
|
|
|
|
}
|
|
|
|
// sendLine sends the given string as a line input to the CLI command. It
|
|
// appends a line feed to the end of string in order to mimic the user hitting
|
|
// the return key.
|
|
func (c *capture) sendLine(s string) {
|
|
// Add a line feed before putting s on the channel in order to mimic the
|
|
// user hitting the return key.
|
|
c.ch <- s + "\n"
|
|
}
|
|
|
|
// Read is read by the CLI in place of user input. It effectively sends lines of
|
|
// input to the CLI, getting each line to be sent off the channel.
|
|
func (c *capture) Read(b []byte) (n int, err error) {
|
|
s := <-c.ch
|
|
return strings.NewReader(s).Read(b)
|
|
}
|
|
|
|
func (c *capture) Close() error {
|
|
close(c.ch)
|
|
return nil
|
|
}
|
|
|
|
// Write is called with anything written to output. This would included results
|
|
// from calling Query() under normal, non-testing conditions, as well as other
|
|
// informational text sent to output, such as the splash message.
|
|
func (c *capture) Write(b []byte) (n int, err error) {
|
|
return 0, nil
|
|
}
|
|
|
|
// Query is called by the CLI command once a full SQL statement is received
|
|
// (signified by the terminator: `;`).
|
|
func (c *capture) Query(org string, db string, sql io.Reader) (*featurebase.WireQueryResponse, error) {
|
|
tmpBuf := new(strings.Builder)
|
|
_, err := io.Copy(tmpBuf, sql)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c.mu.Lock()
|
|
c.sqls = append(c.sqls, tmpBuf.String())
|
|
c.mu.Unlock()
|
|
|
|
select {
|
|
case c.queryDone <- struct{}{}:
|
|
case <-c.asserting:
|
|
c.mu.Lock()
|
|
c.err = errors.Errorf("unexpected query: %s", sql)
|
|
c.mu.Unlock()
|
|
}
|
|
return &featurebase.WireQueryResponse{}, nil
|
|
}
|