From 37ee6ea482b93a1a413bc55f17226c72389696b9 Mon Sep 17 00:00:00 2001 From: Travis Turner Date: Mon, 13 Mar 2023 16:29:18 -0500 Subject: [PATCH] fbsql integration test framework (#2308) * 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 --- cli/Makefile | 15 ++ cli/cli.go | 41 +++-- cli/cli_integration_test.go | 255 +++++++++++++++++++++++++++++ cli/cli_test.go | 6 +- cli/kafka.go | 2 +- cli/meta.go | 50 +++++- cli/splitter.go | 7 +- cli/testdata/database | 45 +++++ cli/testdata/famous.csv | 10 ++ cli/testdata/meta_bang | 8 + cli/testdata/meta_cd | 17 ++ cli/testdata/meta_echo | 6 + cli/testdata/meta_file | 70 ++++++++ cli/testdata/meta_include | 24 +++ cli/testdata/meta_output | 67 ++++++++ cli/testdata/meta_pset_border | 52 ++++++ cli/testdata/meta_pset_expanded | 35 ++++ cli/testdata/meta_pset_tuples_only | 34 ++++ cli/testdata/meta_set | 31 ++++ cli/testdata/meta_timing | 50 ++++++ cli/testdata/meta_write | 24 +++ cli/testdata/people.sql | 12 ++ cli/testdata/query_buffer | 40 +++++ cli/testdata/setup | 50 ++++++ cli/testdata/table | 60 +++++++ cli/writer.go | 4 +- 26 files changed, 990 insertions(+), 25 deletions(-) create mode 100644 cli/Makefile create mode 100644 cli/cli_integration_test.go create mode 100644 cli/testdata/database create mode 100644 cli/testdata/famous.csv create mode 100644 cli/testdata/meta_bang create mode 100644 cli/testdata/meta_cd create mode 100644 cli/testdata/meta_echo create mode 100644 cli/testdata/meta_file create mode 100644 cli/testdata/meta_include create mode 100644 cli/testdata/meta_output create mode 100644 cli/testdata/meta_pset_border create mode 100644 cli/testdata/meta_pset_expanded create mode 100644 cli/testdata/meta_pset_tuples_only create mode 100644 cli/testdata/meta_set create mode 100644 cli/testdata/meta_timing create mode 100644 cli/testdata/meta_write create mode 100644 cli/testdata/people.sql create mode 100644 cli/testdata/query_buffer create mode 100644 cli/testdata/setup create mode 100644 cli/testdata/table diff --git a/cli/Makefile b/cli/Makefile new file mode 100644 index 000000000..b06eaba45 --- /dev/null +++ b/cli/Makefile @@ -0,0 +1,15 @@ +.PHONY: test testv test-integration testv-integration + +GO=go + +test: + $(GO) test ./... -short + +testv: + $(GO) test -v ./... -short + +test-integration: + $(GO) test . -count 1 -timeout 20m -run TestCLIIntegration/$(RUN) + +testv-integration: + $(GO) test -v . -count 1 -timeout 20m -run TestCLIIntegration/$(RUN) diff --git a/cli/cli.go b/cli/cli.go index 428eeca9f..ba4f48bc5 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -56,9 +56,9 @@ type Command struct { Queryer Queryer `json:"-"` - Stdin io.ReadCloser `json:"-"` - Stdout io.Writer `json:"-"` - Stderr io.Writer `json:"-"` + stdin io.ReadCloser `json:"-"` + stdout io.Writer `json:"-"` + stderr io.Writer `json:"-"` // output is where actual results are written. This might point to stdout, // or to a file, based on the current configuration. @@ -116,9 +116,9 @@ func NewCommand(logdest logger.Logger) *Command { splitter: newSplitter(newReplacer(variables)), workingDir: newWorkingDir(), - Stdin: Stdin, - Stdout: Stdout, - Stderr: Stderr, + stdin: Stdin, + stdout: Stdout, + stderr: Stderr, output: Stdout, writeOptions: defaultWriteOptions(), @@ -129,6 +129,23 @@ func NewCommand(logdest logger.Logger) *Command { } } +// SetStdin sets stdin. This is useful for initial configuration in tests. +func (cmd *Command) SetStdin(rc io.ReadCloser) { + cmd.stdin = rc +} + +// SetStdout sets both stdout and output to the value provided. This is useful +// for initial configuration in tests. +func (cmd *Command) SetStdout(w io.Writer) { + cmd.stdout = w + cmd.output = w +} + +// SetStderr sets stderr. This is useful for initial configuration in tests. +func (cmd *Command) SetStderr(w io.Writer) { + cmd.stderr = w +} + // Run is the main entry-point to the CLI. func (cmd *Command) Run(ctx context.Context) error { if err := cmd.run(ctx); err != nil { @@ -209,9 +226,9 @@ func (cmd *Command) run(ctx context.Context) error { HistoryLimit: 100000, DisableAutoSaveHistory: true, - Stdin: cmd.Stdin, - Stdout: cmd.Stdout, - Stderr: cmd.Stderr, + Stdin: cmd.stdin, + Stdout: cmd.stdout, + Stderr: cmd.stderr, }) if err != nil { return errors.Wrap(err, "getting readline") @@ -376,7 +393,7 @@ func (cmd *Command) executeAndWriteQuery(qry query) error { } return errors.Wrap(err, "making query") } - if err := writeTable(queryResponse, cmd.writeOptions, cmd.output, cmd.Stdout, cmd.Stderr); err != nil { + if err := writeTable(queryResponse, cmd.writeOptions, cmd.output, cmd.stdout, cmd.stderr); err != nil { return errors.Wrap(err, "writing out response") } @@ -421,7 +438,7 @@ func (n *nopPrinter) Errorf(format string, a ...any) {} // Printf is a helper method which sends the given payload to stdout. func (cmd *Command) Printf(format string, a ...any) { out := fmt.Sprintf(format, a...) - cmd.Stdout.Write([]byte(out)) + cmd.stdout.Write([]byte(out)) } // Outputf is a helper method which sends the given payload to output. @@ -433,7 +450,7 @@ func (cmd *Command) Outputf(format string, a ...any) { // Errorf is a helper method which sends the given payload to stderr. func (cmd *Command) Errorf(format string, a ...any) { out := fmt.Sprintf(format, a...) - cmd.Stderr.Write([]byte(out)) + cmd.stderr.Write([]byte(out)) } func (cmd *Command) setupHistory() { diff --git a/cli/cli_integration_test.go b/cli/cli_integration_test.go new file mode 100644 index 000000000..0a063e315 --- /dev/null +++ b/cli/cli_integration_test.go @@ -0,0 +1,255 @@ +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 +} diff --git a/cli/cli_test.go b/cli/cli_test.go index bf02013d5..17b82c5ee 100644 --- a/cli/cli_test.go +++ b/cli/cli_test.go @@ -22,8 +22,8 @@ func TestCLI(t *testing.T) { capture := newCapture(t) cli := cli.NewCommand(logger.StderrLogger) - cli.Stdin = capture - cli.Stdout = capture + cli.SetStdin(capture) + cli.SetStdout(capture) cli.Queryer = capture go func() { @@ -74,7 +74,7 @@ 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 the +// also captures calls made to the Queryer.Query method and ensures the sql they // contain is expected. type capture struct { t *testing.T diff --git a/cli/kafka.go b/cli/kafka.go index a111c8785..9908dd56d 100644 --- a/cli/kafka.go +++ b/cli/kafka.go @@ -65,6 +65,6 @@ func (cmd *Command) newKafkaRunner(cfgFile string) (*kafka.Runner, error) { return kafka.NewRunner( idkCfg, batch.NewSQLBatcher(cmd, flds), - cmd.Stderr, + cmd.stderr, ), nil } diff --git a/cli/meta.go b/cli/meta.go index f3a268f1c..040f5d7c1 100644 --- a/cli/meta.go +++ b/cli/meta.go @@ -81,7 +81,7 @@ func (m *metaBang) execute(cmd *Command) (responseAction, error) { } c := exec.Command(m.args[0]) c.Args = m.args - c.Stdout = cmd.Stdout + c.Stdout = cmd.stdout err := c.Run() return actionNone, errors.Wrap(err, "running bang command") } @@ -192,7 +192,7 @@ func newMetaEcho(args []string) *metaEcho { } func (m *metaEcho) execute(cmd *Command) (responseAction, error) { - return echo(m.args, cmd.Stdout) + return echo(m.args, cmd.stdout) } func echo(args []string, w io.Writer) (responseAction, error) { @@ -335,7 +335,7 @@ Informational Formatting \pset [NAME [VALUE]] set table output option - (border|expanded|tuples_only) + (border|expanded|location|tuples_only) \t [on|off] show only rows \x [on|off] toggle expanded output @@ -491,6 +491,37 @@ func (m *metaListViews) execute(cmd *Command) (responseAction, error) { return actionReset, nil } +// //////////////////////////////////////////////////////////////////////////// +// location (sub-command of pset) +// //////////////////////////////////////////////////////////////////////////// +type metaLocation struct { + args []string +} + +func newMetaLocation(args []string) *metaLocation { + return &metaLocation{ + args: args, + } +} + +func (m *metaLocation) execute(cmd *Command) (responseAction, error) { + switch len(m.args) { + case 0: + // pass + case 1: + loc, err := time.LoadLocation(m.args[0]) + if err != nil { + return actionNone, errors.Wrapf(err, "loading location: %s", m.args[0]) + } + cmd.writeOptions.location = loc + default: + return actionNone, errors.Errorf("meta command 'location' takes zero or one argument") + } + + cmd.Printf("Location is %s.\n", cmd.writeOptions.location) + return actionNone, nil +} + // //////////////////////////////////////////////////////////////////////////// // org // //////////////////////////////////////////////////////////////////////////// @@ -542,7 +573,7 @@ func (m *metaOutput) execute(cmd *Command) (responseAction, error) { } // Set cmd.output to cmd.Stdout. - cmd.output = cmd.Stdout + cmd.output = cmd.stdout return actionNone, nil @@ -603,12 +634,14 @@ func (m *metaPSet) print(cmd *Command) { fmt := `border %d expanded %s +location %s tuples_only %s ` cmd.Printf(fmt, cmd.writeOptions.border, onOff(cmd.writeOptions.expanded), + cmd.writeOptions.location, onOff(cmd.writeOptions.tuplesOnly), ) @@ -627,6 +660,9 @@ func (m *metaPSet) execute(cmd *Command) (responseAction, error) { case "expanded", "x": sub := newMetaExpanded(m.args[1:]) return sub.execute(cmd) + case "location": + sub := newMetaLocation(m.args[1:]) + return sub.execute(cmd) case "tuples_only", "t": sub := newMetaTuplesOnly(m.args[1:]) return sub.execute(cmd) @@ -678,7 +714,7 @@ func newMetaReset() *metaReset { } func (m *metaReset) execute(cmd *Command) (responseAction, error) { - cmd.Printf(cmd.buffer.reset()) + cmd.Printf(cmd.buffer.reset() + "\n") return actionReset, nil } @@ -746,7 +782,7 @@ func (m *metaTiming) execute(cmd *Command) (responseAction, error) { case "off": cmd.writeOptions.timing = false default: - return actionNone, errors.Errorf("unrecognized value \"%s\" for \"\timing\": Boolean expected", m.args[0]) + return actionNone, errors.Errorf("unrecognized value \"%s\" for \"\\timing\": Boolean expected", m.args[0]) } default: return actionNone, errors.Errorf("meta command 'timing' takes zero or one argument") @@ -843,7 +879,7 @@ func newMetaWarn(args []string) *metaWarn { } func (m *metaWarn) execute(cmd *Command) (responseAction, error) { - return echo(m.args, cmd.Stderr) + return echo(m.args, cmd.stderr) } // //////////////////////////////////////////////////////////////////////////// diff --git a/cli/splitter.go b/cli/splitter.go index bb3c5f2d1..9bc1c8720 100644 --- a/cli/splitter.go +++ b/cli/splitter.go @@ -30,7 +30,12 @@ func newSplitter(r *replacer) *splitter { // 2- [metaCommands...]: "\! pwd \q" // 3- [queryParts...][metaCommands...]: "select * from \i file.sql" func (s *splitter) split(line string) ([]queryPart, []metaCommand, error) { - // Look for a meta command + // Look for a comment line. + if strings.HasPrefix(line, "--") { + return nil, nil, nil + } + + // Look for a meta command. parts := strings.SplitN(line, `\`, 2) switch len(parts) { diff --git a/cli/testdata/database b/cli/testdata/database new file mode 100644 index 000000000..65f59cefa --- /dev/null +++ b/cli/testdata/database @@ -0,0 +1,45 @@ +// Show databases now that we have set org. +SEND:SHOW DATABASES; +EXPECT:+-----+------+-------+------------+------------+------------+-------+-------------+ +EXPECT:| _id | name | owner | updated_by | created_at | updated_at | units | description | +EXPECT:+-----+------+-------+------------+------------+------------+-------+-------------+ +EXPECT:+-----+------+-------+------------+------------+------------+-------+-------------+ +EXPECT: + +// Create db1. +SEND:CREATE DATABASE db1 WITH UNITS 1; +EXPECT: + +// List databases via SHOW DATABASES. +SEND:SHOW DATABASES; +EXPECT:+--------------------------------------+------+-------+------------+----------------------+----------------------+-------+-------------+ +EXPECT:| _id | name | owner | updated_by | created_at | updated_at | units | description | +EXPECT:+--------------------------------------+------+-------+------------+----------------------+----------------------+-------+-------------+ +EXPECTCOMP:WithFormat:| {uuid} | db1 | | | {timestamp} | {timestamp} | 1 | | +EXPECT:+--------------------------------------+------+-------+------------+----------------------+----------------------+-------+-------------+ +EXPECT: + +// List databases via SHOW DATABASES. +SEND:\l +EXPECT:+--------------------------------------+------+-------+------------+----------------------+----------------------+-------+-------------+ +EXPECT:| _id | name | owner | updated_by | created_at | updated_at | units | description | +EXPECT:+--------------------------------------+------+-------+------------+----------------------+----------------------+-------+-------------+ +EXPECTCOMP:WithFormat:| {uuid} | db1 | | | {timestamp} | {timestamp} | 1 | | +EXPECT:+--------------------------------------+------+-------+------------+----------------------+----------------------+-------+-------------+ +EXPECT: + +// Check database connection. +SEND:\c +EXPECT:You are not connected to a database. + +// Try connecting to an invalid database. +SEND:\c invalid +EXPECT:executing meta command: invalid database: invalid + +// Try connecting with too many arguments. +SEND:\c db1 extra +EXPECT:executing meta command: meta command 'connect' takes zero or one argument + +// Connect to a database. +SEND:\c db1 +EXPECTCOMP:WithFormat:You are now connected to database "db1" ({uuid}). diff --git a/cli/testdata/famous.csv b/cli/testdata/famous.csv new file mode 100644 index 000000000..addcff4dc --- /dev/null +++ b/cli/testdata/famous.csv @@ -0,0 +1,10 @@ + +"Id", "Name", "Short description", "Gender", "Country", "Occupation", "Birth year", "Death year", "Manner of death", "Age of death" +1, "George Washington", "1st president of the United States (1732–1799)", "Male", "United States of America; Kingdom of Great Britain", "Politician", "1732", "1799", "natural causes", "67" +2, "Douglas Adams", "English writer and humorist", "Male", "United Kingdom", "Artist", "1952", "2001", "natural causes", "49" +3, "Abraham Lincoln", "16th president of the United States (1809-1865)", "Male", "United States of America", "Politician", "1809", "1865", "homicide", "56" +4, "Wolfgang Amadeus Mozart", "Austrian composer of the Classical period", "Male", "Archduchy of Austria; Archbishopric of Salzburg", "Artist", "1756", "1791", "0", "35" +5, "Ludwig van Beethoven", "German classical and romantic composer", "Male", "Holy Roman Empire; Austrian Empire", "Artist", "1770", "1827", "0", "57" +6, "Jean-François Champollion", "French classical scholar", "Male", "Kingdom of France; First French Empire", "Egyptologist", "1790", "1832", "natural causes", "42" +7, "Paul Morand", "French writer", "Male", "France", "Artist", "1888", "1976", "0", "88" +8, "Claude Monet", "French impressionist painter (1840-1926)", "Male", "France", "Artist", "1840", "1926", "natural causes", "86" diff --git a/cli/testdata/meta_bang b/cli/testdata/meta_bang new file mode 100644 index 000000000..1a21489ca --- /dev/null +++ b/cli/testdata/meta_bang @@ -0,0 +1,8 @@ +SEND:\! echo 'foo' +EXPECT:foo + +SEND:\! echo "foo" +EXPECT:"foo" + +SEND:\! +EXPECT:executing meta command: meta command '!' requires at least one argument diff --git a/cli/testdata/meta_cd b/cli/testdata/meta_cd new file mode 100644 index 000000000..7fb3e597e --- /dev/null +++ b/cli/testdata/meta_cd @@ -0,0 +1,17 @@ +// Make a directory so we can test \cd'ing into it. +SEND:\! mkdir cli-test-dir +SEND:\cd cli-test-dir +SEND:\cd .. +SEND:\! rmdir cli-test-dir + +// TODO(tlt): before we do this, we should implement the ability to execute +// commands in a \set like: +// \set homedir `pwd` +// then we can store what directory we're in so we can move back to it +// at the end of the test +// Switch to home directory. +// SEND:\cd + +// Expect error on extra argument to \cd. +SEND:\cd dir extra +EXPECT:executing meta command: meta command 'cd' takes zero or one argument diff --git a/cli/testdata/meta_echo b/cli/testdata/meta_echo new file mode 100644 index 000000000..6995842e8 --- /dev/null +++ b/cli/testdata/meta_echo @@ -0,0 +1,6 @@ +SEND:\echo +EXPECT: + +// Simple \echo. +SEND:\echo foo bar +EXPECT:foo bar diff --git a/cli/testdata/meta_file b/cli/testdata/meta_file new file mode 100644 index 000000000..f0fdc5647 --- /dev/null +++ b/cli/testdata/meta_file @@ -0,0 +1,70 @@ +// Create a table. +SEND:CREATE TABLE famous ( +SEND: _id ID, +SEND: name STRING, +SEND: description STRING, +SEND: gender STRING, +SEND: country STRING, +SEND: occupation STRING, +SEND: birth_year INT min -32767 max 32767, +SEND: death_year INT min -32767 max 32767, +SEND: death_manner STRING, +SEND: birth_age INT min -32767 max 32767 +SEND:); +EXPECT: + +// Open bulk insert. +SEND:BULK INSERT +SEND:INTO famous (_id, name, description, gender, country, occupation, +SEND: birth_year, death_year, death_manner, birth_age ) +SEND:MAP(0 INT, +SEND:1 STRING, +SEND:2 STRING, +SEND:3 STRING, +SEND:4 STRING, +SEND:5 STRING, +SEND:6 INT, +SEND:7 INT, +SEND:8 STRING, +SEND:9 INT ) +SEND:FROM +SEND: x' + +// Call \file +SEND:\file testdata/famous.csv + +// Close bulk insert. +SEND:' +SEND:WITH +SEND: BATCHSIZE 100000 +SEND: FORMAT 'CSV' +SEND: INPUT 'STREAM' +SEND: HEADER_ROW; +EXPECT: + +// Query table to ensure we have data. +SEND:SELECT * FROM famous; +EXPECT:+-----+---------------------------+-------------------------------------------------+--------+----------------------------------------------------+--------------+------------+------------+----------------+-----------+ +EXPECT:| _id | name | description | gender | country | occupation | birth_year | death_year | death_manner | birth_age | +EXPECT:+-----+---------------------------+-------------------------------------------------+--------+----------------------------------------------------+--------------+------------+------------+----------------+-----------+ +EXPECT:| 1 | George Washington | 1st president of the United States (1732–1799) | Male | United States of America; Kingdom of Great Britain | Politician | 1732 | 1799 | natural causes | 67 | +EXPECT:| 2 | Douglas Adams | English writer and humorist | Male | United Kingdom | Artist | 1952 | 2001 | natural causes | 49 | +EXPECT:| 3 | Abraham Lincoln | 16th president of the United States (1809-1865) | Male | United States of America | Politician | 1809 | 1865 | homicide | 56 | +EXPECT:| 4 | Wolfgang Amadeus Mozart | Austrian composer of the Classical period | Male | Archduchy of Austria; Archbishopric of Salzburg | Artist | 1756 | 1791 | 0 | 35 | +EXPECT:| 5 | Ludwig van Beethoven | German classical and romantic composer | Male | Holy Roman Empire; Austrian Empire | Artist | 1770 | 1827 | 0 | 57 | +EXPECT:| 6 | Jean-François Champollion | French classical scholar | Male | Kingdom of France; First French Empire | Egyptologist | 1790 | 1832 | natural causes | 42 | +EXPECT:| 7 | Paul Morand | French writer | Male | France | Artist | 1888 | 1976 | 0 | 88 | +EXPECT:| 8 | Claude Monet | French impressionist painter (1840-1926) | Male | France | Artist | 1840 | 1926 | natural causes | 86 | +EXPECT:+-----+---------------------------+-------------------------------------------------+--------+----------------------------------------------------+--------------+------------+------------+----------------+-----------+ +EXPECT: + +// TODO(tlt): dropping the table seems to cause problems. +// Drop the table. +//SEND:DROP TABLE famous; + +// Ensure that invalid aruments (none or too many) return an error. +SEND:\file +EXPECT:executing meta command: meta command 'file' requires exactly one argument +SEND:\file filename extra +EXPECT:executing meta command: meta command 'file' requires exactly one argument + diff --git a/cli/testdata/meta_include b/cli/testdata/meta_include new file mode 100644 index 000000000..6ed62ec70 --- /dev/null +++ b/cli/testdata/meta_include @@ -0,0 +1,24 @@ +// Include with no argument should error. +SEND:\i +EXPECT:executing meta command: meta command 'include' requires exactly one argument + +// Include with too many arguments should error. +SEND:\include testdata/people.sql extra +EXPECT:executing meta command: meta command 'include' requires exactly one argument + +// Invalid file should error. +SEND:\include invalid.file +EXPECT:executing meta command: opening file: invalid.file: open invalid.file: no such file or directory + +SEND:\include testdata/people.sql +EXPECT: +EXPECT: +EXPECT:+-----+------+-----+ +EXPECT:| _id | name | age | +EXPECT:+-----+------+-----+ +EXPECT:| 1 | Amy | 42 | +EXPECT:| 2 | Bob | 27 | +EXPECT:| 3 | Carl | 33 | +EXPECT:+-----+------+-----+ +EXPECT: +EXPECT:mix in a meta command diff --git a/cli/testdata/meta_output b/cli/testdata/meta_output new file mode 100644 index 000000000..27d3c88f6 --- /dev/null +++ b/cli/testdata/meta_output @@ -0,0 +1,67 @@ +SEND:SELECT * FROM users; +EXPECT:+-----+-------+-----+ +EXPECT:| _id | name | age | +EXPECT:+-----+-------+-----+ +EXPECT:| 1 | Anne | 38 | +EXPECT:| 2 | Bill | 23 | +EXPECT:| 3 | Cindy | 64 | +EXPECT:+-----+-------+-----+ +EXPECT: + +// Redirect output to a file. +SEND:\o test-output-file +SEND:SELECT * FROM users; + +// Ensure the output went to the file. +SEND:\! cat test-output-file +EXPECT:+-----+-------+-----+ +EXPECT:| _id | name | age | +EXPECT:+-----+-------+-----+ +EXPECT:| 1 | Anne | 38 | +EXPECT:| 2 | Bill | 23 | +EXPECT:| 3 | Cindy | 64 | +EXPECT:+-----+-------+-----+ +EXPECT: + +// Let's test some qecho stuff here while we're at it. +SEND:\qecho string with "double quotes" +SEND:\qecho -n one +SEND:\qecho -n two +SEND:\qecho three +SEND:\qecho four + +SEND:\! cat test-output-file +EXPECT:+-----+-------+-----+ +EXPECT:| _id | name | age | +EXPECT:+-----+-------+-----+ +EXPECT:| 1 | Anne | 38 | +EXPECT:| 2 | Bill | 23 | +EXPECT:| 3 | Cindy | 64 | +EXPECT:+-----+-------+-----+ +EXPECT: +EXPECT:string with "double quotes" +EXPECT:onetwothree +EXPECT:four + +// And \warn messages should still go to stderr, not the file. +SEND:\warn a warning string +EXPECT:a warning string + +// Remove the file. +SEND:\! rm test-output-file + +// Set the output back to stdout. +SEND:\o +SEND:SELECT * FROM users; +EXPECT:+-----+-------+-----+ +EXPECT:| _id | name | age | +EXPECT:+-----+-------+-----+ +EXPECT:| 1 | Anne | 38 | +EXPECT:| 2 | Bill | 23 | +EXPECT:| 3 | Cindy | 64 | +EXPECT:+-----+-------+-----+ +EXPECT: + +// Ensure extra arguments to \output causes an error. +SEND:\o filename extra +EXPECT:executing meta command: meta command 'output' takes zero or one argument diff --git a/cli/testdata/meta_pset_border b/cli/testdata/meta_pset_border new file mode 100644 index 000000000..e089da995 --- /dev/null +++ b/cli/testdata/meta_pset_border @@ -0,0 +1,52 @@ +SEND:\pset border 2 +EXPECT:Border style is 2. + +SEND:SELECT 1 as foo, 'baz' as bar; +EXPECT:+-----+-----+ +EXPECT:| foo | bar | +EXPECT:+-----+-----+ +EXPECT:| 1 | baz | +EXPECT:+-----+-----+ +EXPECT: + +SEND:\pset border +EXPECT:Border style is 2. + +SEND:\pset border 999 +EXPECT:Border style is 0. + +SEND:\pset border 1 +EXPECT:Border style is 1. + +SEND:SELECT 1 as foo, 'baz' as bar; +EXPECT: foo | bar +EXPECT:-----+----- +EXPECT: 1 | baz +EXPECT: + +SEND:\pset border 2 +EXPECT:Border style is 2. + +SEND:SELECT 1 as foo, 'baz' as bar; +EXPECT:+-----+-----+ +EXPECT:| foo | bar | +EXPECT:+-----+-----+ +EXPECT:| 1 | baz | +EXPECT:+-----+-----+ +EXPECT: + +SEND:\pset border 0 +EXPECT:Border style is 0. + +SEND:SELECT 1 as foo, 'baz' as bar; +EXPECT:foo bar +EXPECT:--- --- +EXPECT: 1 baz +EXPECT: + +SEND:\pset border 1 extra +EXPECT:executing meta command: meta command 'pset' takes zero, one, or two arguments + +// Set border back to the testing default. +SEND:\pset border 2 +EXPECT:Border style is 2. diff --git a/cli/testdata/meta_pset_expanded b/cli/testdata/meta_pset_expanded new file mode 100644 index 000000000..e3d7a5f7c --- /dev/null +++ b/cli/testdata/meta_pset_expanded @@ -0,0 +1,35 @@ +// Set to off. +SEND:\pset expanded off +EXPECT:Expanded display is off. + +// Set to on. +SEND:\pset expanded on +EXPECT:Expanded display is on. + +// Toggle to off. +SEND:\pset expanded +EXPECT:Expanded display is off. + +// Toggle to on. +SEND:\pset expanded +EXPECT:Expanded display is on. + +// Set to something invalid. +SEND:\pset expanded invalid +EXPECT:executing meta command: unrecognized value "invalid" for "expanded": Boolean expected + +// Ensure expanded shows results vertically. +SEND:SELECT 1 as foo, 'baz' as bar; +EXPECT:+-----+-----+ +EXPECT:| foo | 1 | +EXPECT:| bar | baz | +EXPECT:+-----+-----+ +EXPECT: + +// Set back to off as we started. +SEND:\pset expanded off +EXPECT:Expanded display is off. + +// make sure the \x meta-command returns expected errors +SEND:\x on extra +EXPECT:executing meta command: meta command 'expanded' takes zero or one argument diff --git a/cli/testdata/meta_pset_tuples_only b/cli/testdata/meta_pset_tuples_only new file mode 100644 index 000000000..03c61943c --- /dev/null +++ b/cli/testdata/meta_pset_tuples_only @@ -0,0 +1,34 @@ +// Set to off. +SEND:\pset tuples_only off +EXPECT:Tuples only is off. + +// Set to on. +SEND:\pset tuples_only on +EXPECT:Tuples only is on. + +// Toggle to off. +SEND:\pset tuples_only +EXPECT:Tuples only is off. + +// Toggle to on. +SEND:\pset tuples_only +EXPECT:Tuples only is on. + +// Set to something invalid. +SEND:\pset tuples_only invalid +EXPECT:executing meta command: unrecognized value "invalid" for "tuples_only": Boolean expected + +// Ensure tuples_only shows only tuples. +SEND:SELECT 1 as foo, 'baz' as bar; +EXPECT:+---+-----+ +EXPECT:| 1 | baz | +EXPECT:+---+-----+ +EXPECT: + +// Set back to off as we started. +SEND:\pset tuples_only off +EXPECT:Tuples only is off. + +// make sure the \t meta-command returns expected errors +SEND:\t off extra +EXPECT:executing meta command: meta command 'tuples_only' takes zero or one argument diff --git a/cli/testdata/meta_set b/cli/testdata/meta_set new file mode 100644 index 000000000..c8b165743 --- /dev/null +++ b/cli/testdata/meta_set @@ -0,0 +1,31 @@ +SEND:\set + +SEND:\set var1 foo +SEND:\set +EXPECT:var1 = 'foo' + +SEND:\set var2 bar +SEND:\set +EXPECT:var1 = 'foo' +EXPECT:var2 = 'bar' + +SEND:\set var3 zoo +SEND:\set +EXPECT:var1 = 'foo' +EXPECT:var2 = 'bar' +EXPECT:var3 = 'zoo' + +SEND:\unset +EXPECT:\unset: missing required argument + +SEND:\unset non-existent-key + +SEND:\unset var1 +SEND:\set +EXPECT:var2 = 'bar' +EXPECT:var3 = 'zoo' + +SEND:\unset var2 extra +EXPECT:\unset: extra argument "extra" ignored +SEND:\set +EXPECT:var3 = 'zoo' diff --git a/cli/testdata/meta_timing b/cli/testdata/meta_timing new file mode 100644 index 000000000..9ee2f47e3 --- /dev/null +++ b/cli/testdata/meta_timing @@ -0,0 +1,50 @@ +// Start by ensuring timing is off. +SEND:\timing off +EXPECT:Timing is off. + +// Set timing on. +SEND:\timing on +EXPECT:Timing is on. + +// Toggle timing. +SEND:\timing +EXPECT:Timing is off. + +// Toggle timing again. +SEND:\timing +EXPECT:Timing is on. + +// Send extra argument to \timing. +SEND:\timing on extra +EXPECT:executing meta command: meta command 'timing' takes zero or one argument + + +SEND:SELECT * from users; +EXPECT:+-----+-------+-----+ +EXPECT:| _id | name | age | +EXPECT:+-----+-------+-----+ +EXPECT:| 1 | Anne | 38 | +EXPECT:| 2 | Bill | 23 | +EXPECT:| 3 | Cindy | 64 | +EXPECT:+-----+-------+-----+ +EXPECT: +EXPECTCOMP:HasPrefix:Execution time: + +// Turn timing back off. +SEND:\timing off +EXPECT:Timing is off. + +// Ensure we don't get timing. +SEND:SELECT * from users; +EXPECT:+-----+-------+-----+ +EXPECT:| _id | name | age | +EXPECT:+-----+-------+-----+ +EXPECT:| 1 | Anne | 38 | +EXPECT:| 2 | Bill | 23 | +EXPECT:| 3 | Cindy | 64 | +EXPECT:+-----+-------+-----+ +EXPECT: + +// Ensure an invalid timing value returns an error. +SEND:\timing invalid +EXPECT:executing meta command: unrecognized value "invalid" for "\timing": Boolean expected diff --git a/cli/testdata/meta_write b/cli/testdata/meta_write new file mode 100644 index 000000000..321059e37 --- /dev/null +++ b/cli/testdata/meta_write @@ -0,0 +1,24 @@ +// Make sure there's something in the query buffer. +// This is left unterminated because we don't need to execute the query; +// we just need there to be something in the buffer. +SEND:SELECT * FROM invalid-table +SEND:\write query-buffer-contents + +// Reset the buffer. +SEND:\r +EXPECT:Query buffer reset (cleared). + +// Read from the file. +SEND:\! cat query-buffer-contents +EXPECT:SELECT * FROM invalid-table + +// Remove the file. +SEND:\! rm query-buffer-contents + +// Send \write with no arguments. +SEND:\write +EXPECT:\w: missing required argument + +// Send \write with extra arguments. +SEND:\write filename extra +EXPECT:executing meta command: meta command 'w' exactly one argument diff --git a/cli/testdata/people.sql b/cli/testdata/people.sql new file mode 100644 index 000000000..7a4c27c1b --- /dev/null +++ b/cli/testdata/people.sql @@ -0,0 +1,12 @@ +-- Create a table. +create table people (_id id, name string, age int); + +-- Insert some values. +insert into people values (1, 'Amy', 42), (2, 'Bob', 27), (3, 'Carl', 33); + +-- Get all rows from the table. +select * from people; + +-- Mix in a meta-command to show that both are supported +-- in the include file. +\echo mix in a meta command diff --git a/cli/testdata/query_buffer b/cli/testdata/query_buffer new file mode 100644 index 000000000..da7719c0b --- /dev/null +++ b/cli/testdata/query_buffer @@ -0,0 +1,40 @@ +SEND:select 1 as foo; +EXPECT:+-----+ +EXPECT:| foo | +EXPECT:+-----+ +EXPECT:| 1 | +EXPECT:+-----+ +EXPECT: + +SEND:\p +EXPECT:select 1 as foo; + +SEND:select 2 +SEND:\p +EXPECT:select 2 +SEND:\r +EXPECT:Query buffer reset (cleared). + +SEND:\p +EXPECT:select 1 as foo; + +SEND:select 3 +SEND:\p +EXPECT:select 3 + +SEND:as foo +SEND:\p +EXPECT:select 3 +EXPECT:as foo + +SEND:; +EXPECT:+-----+ +EXPECT:| foo | +EXPECT:+-----+ +EXPECT:| 3 | +EXPECT:+-----+ +EXPECT: + +SEND:\p +EXPECT:select 3 +EXPECT:as foo; diff --git a/cli/testdata/setup b/cli/testdata/setup new file mode 100644 index 000000000..f5c901ae8 --- /dev/null +++ b/cli/testdata/setup @@ -0,0 +1,50 @@ +// Startup splash. +EXPECT:FeatureBase CLI () +EXPECT:Type "\q" to quit. +EXPECT:Detected on-prem, serverless deployment. +EXPECTCOMP:HasPrefix:Host: http://localhost: +EXPECT:You are not connected to a database. + +// Show databases. +SEND:SHOW DATABASES; +EXPECT:Organization required. Use \org to set an organization. + +// Get current org. +SEND:\org +EXPECT:You have not set an organization. + +// Set org. +SEND:\org acme +EXPECT:You have set organization "acme". + +// Try to set org with too many arguments. +SEND:\org acme extra +EXPECT:executing meta command: meta command 'org' takes zero or one argument + +// Set location to UTC so that expected timestamp size is consistent. +// Without this, a test running locally in may have a timestamp that +// ends in a timezone offset such as `-06:00`, while one running as UTC +// will have `Z`. Since these string lengths differ, our generic +// {timestamp} comparison will fail. +SEND:\pset location UTC +EXPECT:Location is UTC. + +// Set an invalid location. +SEND:\pset location invalid +EXPECT:executing meta command: loading location: invalid: unknown time zone invalid + +// Try to set location with too many arguments. +SEND:\pset location UTC extra +EXPECT:executing meta command: meta command 'pset' takes zero, one, or two arguments + +// Set border to 2 for testing because it makes it easier to visually see +// what the tests are expecting (because lines don't end in spaces). +SEND:\pset border 2 +EXPECT:Border style is 2. + +// Check the state of pset. +SEND:\pset +EXPECT:border 2 +EXPECT:expanded off +EXPECT:location UTC +EXPECT:tuples_only off diff --git a/cli/testdata/table b/cli/testdata/table new file mode 100644 index 000000000..0bcccdef7 --- /dev/null +++ b/cli/testdata/table @@ -0,0 +1,60 @@ +// Show tables for database using SHOW TABLES. +SEND:SHOW TABLES; +EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+ +EXPECT:| _id | name | owner | updated_by | created_at | updated_at | keys | space_used | description | +EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+ +EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | | +EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | | +EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | | +EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | | +EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | | +EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+ +EXPECT: + +// Show tables for database using \dt. +SEND:\dt +EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+ +EXPECT:| _id | name | owner | updated_by | created_at | updated_at | keys | space_used | description | +EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+ +EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | | +EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | | +EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | | +EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | | +EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | | +EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+ +EXPECT: + +// Create a table. That can be used for general testing. +SEND:CREATE TABLE users (_id id, name string, age int); +EXPECT: +SEND:INSERT INTO users VALUES (1, 'Anne', 38), (2, 'Bill', 23), (3, 'Cindy', 64); +EXPECT: + + +// Show tables for database to get the newly created table. +SEND:\dt +EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+ +EXPECT:| _id | name | owner | updated_by | created_at | updated_at | keys | space_used | description | +EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+ +EXPECTCOMP:WithFormat:| users | users | | | {timestamp} | {timestamp} | false | 0 | | +EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | | +EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | | +EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | | +EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | | +EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | | +EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+ +EXPECT: + +// We don't select from users until AFTER we check SHOW TABLES above because +// running this creates the fb_views sytem table which has a description. +// And it's annoying to mask out all of the description fields because we +// don't know which row fb_views will fall into. +SEND:SELECT * from users; +EXPECT:+-----+-------+-----+ +EXPECT:| _id | name | age | +EXPECT:+-----+-------+-----+ +EXPECT:| 1 | Anne | 38 | +EXPECT:| 2 | Bill | 23 | +EXPECT:| 3 | Cindy | 64 | +EXPECT:+-----+-------+-----+ +EXPECT: diff --git a/cli/writer.go b/cli/writer.go index 7ee68c93c..ca2f7a2db 100644 --- a/cli/writer.go +++ b/cli/writer.go @@ -18,6 +18,7 @@ type writeOptions struct { expanded bool timing bool tuplesOnly bool + location *time.Location } func defaultWriteOptions() *writeOptions { @@ -26,6 +27,7 @@ func defaultWriteOptions() *writeOptions { expanded: false, timing: false, tuplesOnly: false, + location: time.Local, } } @@ -101,7 +103,7 @@ func writeTable(r *featurebase.WireQueryResponse, format *writeOptions, qOut io. case nil: row[i] = nullValue case time.Time: - row[i] = v.Format(time.RFC3339Nano) + row[i] = v.In(format.location).Format(time.RFC3339Nano) } } t.AppendRow(table.Row(row))