featurebase/cmd/root_test.go
2022-12-12 09:01:20 -08:00

160 lines
4.9 KiB
Go

// Copyright 2022 Molecula Corp. (DBA FeatureBase).
// SPDX-License-Identifier: Apache-2.0
package cmd_test
import (
"bytes"
"fmt"
"io"
"os"
"reflect"
"strings"
"testing"
"github.com/featurebasedb/featurebase/v3/cmd"
"github.com/featurebasedb/featurebase/v3/testhook"
"github.com/spf13/cobra"
)
// failErr calls t.Fatal if err != nil and adds the optional context to the
// error message.
func failErr(t *testing.T, err error, context ...string) {
ctx := strings.Join(context, "; ")
if err != nil {
t.Fatal(ctx, ": ", err)
}
}
// ExecNewRootCommand executes the pilosa root command with the given arguments
// and returns its output. It will fail if the command does not complete within
// 1 second.
func ExecNewRootCommand(t *testing.T, args ...string) (string, error) {
buf := &bytes.Buffer{}
rc := cmd.NewRootCommand(buf)
rc.SetArgs(args)
err := rc.Execute()
return buf.String(), err
}
// validator is a simple helper to avoid repeated `if err != nil` checks in
// validation code. One can use it to check that several pairs of things are
// equal, and at the end access an informative error message about the first
// non-equal pair encountered (or nil if all were equal)
type validator struct {
err error
}
// Check that two things are equal, and if not set v.err to a descriptive error
// message.
func (v *validator) Check(actual, expected interface{}) {
if v.err != nil {
return
}
if !reflect.DeepEqual(actual, expected) {
v.err = fmt.Errorf("Actual: '%v' is not equal to '%v'", actual, expected)
}
}
// Error returns the validator's error value if any v.Check call found an error.
func (v *validator) Error() error { return v.err }
// commandTest represents all possible ways to configure a pilosa command, as
// well as a function for validating whether the command worked as expected.
// args should be set to everything that comes after "pilosa" on the comand
// line.
type commandTest struct {
args []string
env map[string]string
cfgFileContent string
validation func() error
}
// executeDry sets up and executes each commandTest with the --dry-run flag set
// to true, and then executes the tests validation function. This stops
// execution after PersistentPreRunE (and so before the command's Run or RunE
// function is called). This is useful for verifying that configuration happened
// properly.
func executeDry(t *testing.T, tests []commandTest) {
for i, test := range tests {
test.args = append(test.args[:1], append([]string{"--dry-run"}, test.args[1:]...)...)
com := test.setupCommand(t)
err := com.Execute()
if err.Error() != "dry run" {
t.Fatalf("Problem with test %d, err: '%v'", i, err)
}
if err := test.validation(); err != nil {
t.Fatalf("Failed test %d due to: %v", i, err)
}
test.reset()
}
}
// setupCommand sets up all the configuration specified in the commandTest so
// that it can be run. This includes setting environment variables, and creating
// a temp config file with the cfgFileContent string as its content.
func (ct *commandTest) setupCommand(t *testing.T) *cobra.Command {
// make config file
cfgFile, err := testhook.TempFile(t, "cmdconf")
failErr(t, err, "making temp file")
_, err = cfgFile.WriteString(ct.cfgFileContent)
failErr(t, err, "writing config to temp file")
// set up config file args/env
ct.env["PILOSA_CONFIG"] = cfgFile.Name()
// set up env
for name, val := range ct.env {
err = os.Setenv(name, val)
failErr(t, err, fmt.Sprintf("setting environment variable '%s' to '%s'", name, val))
}
// address common case where system might have postgres bind
// setting that an existing running Pilosa is using (causing test
// failures due to port conflict)
os.Setenv("PILOSA_POSTGRES_BIND", "")
// make command and set args
rc := cmd.NewRootCommand(io.Discard)
rc.SetArgs(ct.args)
err = cfgFile.Close()
failErr(t, err, "closing config file")
return rc
}
// reset the environment after setup/run of a commandTest.
func (ct *commandTest) reset() {
for name := range ct.env {
os.Setenv(name, "")
}
}
func TestRootCommand(t *testing.T) {
outStr, err := ExecNewRootCommand(t, "--help")
if !strings.Contains(outStr, "Usage:") ||
!strings.Contains(outStr, "Available Commands:") ||
!strings.Contains(outStr, "--help") || err != nil {
t.Fatalf("Expected standard usage message from RootCommand, but err: '%v', output: '%s'", err, outStr)
}
}
func TestRootCommand_Config(t *testing.T) {
file, err := testhook.TempFile(t, "test.conf")
if err != nil {
t.Fatalf("creating config file: %v", err)
}
config := `data-dir = "/tmp/pil5_0"
bind = "127.0.0.1:10101"
[cluster]
replicas = 2
partitions = 128`
if _, err := file.Write([]byte(config)); err != nil {
t.Fatalf("writing config file: %v", err)
}
file.Close()
_, err = ExecNewRootCommand(t, "server", "--config", file.Name())
if err == nil || err.Error() != "invalid option in configuration file: cluster.partitions" {
t.Fatalf("Expected invalid option in configuration file, but err: '%v'", err)
}
}