From 9de9bcb17c7b2ea90670b526356a17af8995b2ee Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Mon, 31 Jan 2022 16:20:14 +0300 Subject: [PATCH] cli: add tests for server commands Fixes: * Proper exit code should be returned on exit for server-related commands --- cli/dump_test.go | 64 +++++++++++++++++--- cli/executor_test.go | 58 +++++++++++++++++- cli/server/server.go | 17 +++--- cli/server/server_test.go | 122 ++++++++++++++++++++++++++++++++++++- cli/server_test.go | 124 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 364 insertions(+), 21 deletions(-) create mode 100644 cli/server_test.go diff --git a/cli/dump_test.go b/cli/dump_test.go index ae2719fff..63cc84936 100644 --- a/cli/dump_test.go +++ b/cli/dump_test.go @@ -4,6 +4,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strconv" "testing" "github.com/nspcc-dev/neo-go/pkg/config" @@ -11,15 +12,19 @@ import ( "gopkg.in/yaml.v2" ) -func TestDBRestore(t *testing.T) { +func TestDBRestoreDump(t *testing.T) { tmpDir := t.TempDir() - chainPath := filepath.Join(tmpDir, "neogotestchain") - cfg, err := config.LoadFile(filepath.Join("..", "config", "protocol.unit_testnet.yml")) - require.NoError(t, err, "could not load config") - cfg.ApplicationConfiguration.DBConfiguration.Type = "leveldb" - cfg.ApplicationConfiguration.DBConfiguration.LevelDBOptions.DataDirectoryPath = chainPath + loadConfig := func(t *testing.T) config.Config { + chainPath := filepath.Join(tmpDir, "neogotestchain") + cfg, err := config.LoadFile(filepath.Join("..", "config", "protocol.unit_testnet.yml")) + require.NoError(t, err, "could not load config") + cfg.ApplicationConfiguration.DBConfiguration.Type = "leveldb" + cfg.ApplicationConfiguration.DBConfiguration.LevelDBOptions.DataDirectoryPath = chainPath + return cfg + } + cfg := loadConfig(t) out, err := yaml.Marshal(cfg) require.NoError(t, err) @@ -29,6 +34,7 @@ func TestDBRestore(t *testing.T) { // generated via `go run ./scripts/gendump/main.go --out ./cli/testdata/chain50x2.acc --blocks 50 --txs 2` const inDump = "./testdata/chain50x2.acc" e := newExecutor(t, false) + stateDump := filepath.Join(tmpDir, "neogo.teststate") baseArgs := []string{"neo-go", "db", "restore", "--unittest", "--config-path", tmpDir, "--in", inDump, "--dump", stateDump} @@ -47,8 +53,50 @@ func TestDBRestore(t *testing.T) { // Dump and compare. dumpPath := filepath.Join(tmpDir, "testdump.acc") - e.Run(t, "neo-go", "db", "dump", "--unittest", - "--config-path", tmpDir, "--out", dumpPath) + + t.Run("missing config", func(t *testing.T) { + e.RunWithError(t, "neo-go", "db", "dump", "--privnet", + "--config-path", tmpDir, "--out", dumpPath) + }) + t.Run("bad logger config", func(t *testing.T) { + badConfigDir := t.TempDir() + logfile := filepath.Join(badConfigDir, "logdir") + require.NoError(t, os.WriteFile(logfile, []byte{1, 2, 3}, os.ModePerm)) + cfg = loadConfig(t) + cfg.ApplicationConfiguration.LogPath = filepath.Join(logfile, "file.log") + out, err = yaml.Marshal(cfg) + require.NoError(t, err) + + cfgPath = filepath.Join(badConfigDir, "protocol.unit_testnet.yml") + require.NoError(t, ioutil.WriteFile(cfgPath, out, os.ModePerm)) + + e.RunWithError(t, "neo-go", "db", "dump", "--unittest", + "--config-path", badConfigDir, "--out", dumpPath) + }) + t.Run("bad storage config", func(t *testing.T) { + badConfigDir := t.TempDir() + logfile := filepath.Join(badConfigDir, "logdir") + require.NoError(t, os.WriteFile(logfile, []byte{1, 2, 3}, os.ModePerm)) + cfg = loadConfig(t) + cfg.ApplicationConfiguration.DBConfiguration.Type = "" + out, err = yaml.Marshal(cfg) + require.NoError(t, err) + + cfgPath = filepath.Join(badConfigDir, "protocol.unit_testnet.yml") + require.NoError(t, ioutil.WriteFile(cfgPath, out, os.ModePerm)) + + e.RunWithError(t, "neo-go", "db", "dump", "--unittest", + "--config-path", badConfigDir, "--out", dumpPath) + }) + + baseCmd := []string{"neo-go", "db", "dump", "--unittest", + "--config-path", tmpDir, "--out", dumpPath} + + t.Run("invalid start/count", func(t *testing.T) { + e.RunWithError(t, append(baseCmd, "--start", "5", "--count", strconv.Itoa(50-5+1+1))...) + }) + + e.Run(t, baseCmd...) d1, err := ioutil.ReadFile(inDump) require.NoError(t, err) diff --git a/cli/executor_test.go b/cli/executor_test.go index 5ff7b2996..a82d8fc32 100644 --- a/cli/executor_test.go +++ b/cli/executor_test.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "math" "strings" + "sync" "testing" "time" @@ -58,13 +59,66 @@ type executor struct { // NetSrv is a network server (can be empty). NetSrv *network.Server // Out contains command output. - Out *bytes.Buffer + Out *ConcurrentBuffer // Err contains command errors. Err *bytes.Buffer // In contains command input. In *bytes.Buffer } +// ConcurrentBuffer is a wrapper over Buffer with mutex. +type ConcurrentBuffer struct { + lock sync.RWMutex + buf *bytes.Buffer +} + +// NewConcurrentBuffer returns new ConcurrentBuffer with underlying buffer initialized. +func NewConcurrentBuffer() *ConcurrentBuffer { + return &ConcurrentBuffer{ + buf: bytes.NewBuffer(nil), + } +} + +// Write is a concurrent wrapper over the corresponding method of bytes.Buffer. +func (w *ConcurrentBuffer) Write(p []byte) (int, error) { + w.lock.Lock() + defer w.lock.Unlock() + + return w.buf.Write(p) +} + +// ReadString is a concurrent wrapper over the corresponding method of bytes.Buffer. +func (w *ConcurrentBuffer) ReadString(delim byte) (string, error) { + w.lock.RLock() + defer w.lock.RUnlock() + + return w.buf.ReadString(delim) +} + +// Bytes is a concurrent wrapper over the corresponding method of bytes.Buffer. +func (w *ConcurrentBuffer) Bytes() []byte { + w.lock.RLock() + defer w.lock.RUnlock() + + return w.buf.Bytes() +} + +// String is a concurrent wrapper over the corresponding method of bytes.Buffer. +func (w *ConcurrentBuffer) String() string { + w.lock.RLock() + defer w.lock.RUnlock() + + return w.buf.String() +} + +// Reset is a concurrent wrapper over the corresponding method of bytes.Buffer. +func (w *ConcurrentBuffer) Reset() { + w.lock.Lock() + defer w.lock.Unlock() + + w.buf.Reset() +} + func newTestChain(t *testing.T, f func(*config.Config), run bool) (*core.Blockchain, *server.Server, *network.Server) { configPath := "../config/protocol.unit_testnet.single.yml" cfg, err := config.LoadFile(configPath) @@ -115,7 +169,7 @@ func newExecutorSuspended(t *testing.T) *executor { func newExecutorWithConfig(t *testing.T, needChain, runChain bool, f func(*config.Config)) *executor { e := &executor{ CLI: newApp(), - Out: bytes.NewBuffer(nil), + Out: NewConcurrentBuffer(), Err: bytes.NewBuffer(nil), In: bytes.NewBuffer(nil), } diff --git a/cli/server/server.go b/cli/server/server.go index 21156cec8..19d3a407a 100644 --- a/cli/server/server.go +++ b/cli/server/server.go @@ -393,11 +393,11 @@ func mkP2PNotary(config network.ServerConfig, chain *core.Blockchain, serv *netw func startServer(ctx *cli.Context) error { cfg, err := getConfigFromContext(ctx) if err != nil { - return err + return cli.NewExitError(err, 1) } log, err := handleLoggingParams(ctx, cfg.ApplicationConfiguration) if err != nil { - return err + return cli.NewExitError(err, 1) } grace, cancel := context.WithCancel(newGraceContext()) @@ -407,7 +407,7 @@ func startServer(ctx *cli.Context) error { chain, prometheus, pprof, err := initBCWithMetrics(cfg, log) if err != nil { - return err + return cli.NewExitError(err, 1) } serv, err := network.NewServer(serverConfig, chain, chain.GetStateSyncModule(), log) @@ -423,15 +423,15 @@ func startServer(ctx *cli.Context) error { oracleSrv, err := mkOracle(serverConfig, chain, serv, log) if err != nil { - return err + return cli.NewExitError(err, 1) } _, err = mkConsensus(serverConfig, chain, serv, log) if err != nil { - return err + return cli.NewExitError(err, 1) } _, err = mkP2PNotary(serverConfig, chain, serv, log) if err != nil { - return err + return cli.NewExitError(err, 1) } rpcServer := server.New(chain, cfg.ApplicationConfiguration.RPC, serv, oracleSrv, log) errChan := make(chan error) @@ -442,7 +442,7 @@ func startServer(ctx *cli.Context) error { sighupCh := make(chan os.Signal, 1) signal.Notify(sighupCh, syscall.SIGHUP) - fmt.Fprintln(ctx.App.Writer, logo()) + fmt.Fprintln(ctx.App.Writer, Logo()) fmt.Fprintln(ctx.App.Writer, serv.UserAgent) fmt.Fprintln(ctx.App.Writer) @@ -517,7 +517,8 @@ func initBlockChain(cfg config.Config, log *zap.Logger) (*core.Blockchain, error return chain, nil } -func logo() string { +// Logo returns Neo-Go logo. +func Logo() string { return ` _ ____________ __________ / | / / ____/ __ \ / ____/ __ \ diff --git a/cli/server/server_test.go b/cli/server/server_test.go index 454076c75..8046a457f 100644 --- a/cli/server/server_test.go +++ b/cli/server/server_test.go @@ -1,7 +1,9 @@ package server import ( + "encoding/binary" "flag" + "io/ioutil" "os" "path/filepath" "testing" @@ -14,6 +16,7 @@ import ( "github.com/stretchr/testify/require" "github.com/urfave/cli" "go.uber.org/zap" + "gopkg.in/yaml.v2" ) // serverTestWD is the default working directory for server tests. @@ -42,6 +45,18 @@ func TestHandleLoggingParams(t *testing.T) { d := t.TempDir() testLog := filepath.Join(d, "file.log") + t.Run("logdir is a file", func(t *testing.T) { + logfile := filepath.Join(d, "logdir") + require.NoError(t, os.WriteFile(logfile, []byte{1, 2, 3}, os.ModePerm)) + set := flag.NewFlagSet("flagSet", flag.ExitOnError) + ctx := cli.NewContext(cli.NewApp(), set, nil) + cfg := config.ApplicationConfiguration{ + LogPath: filepath.Join(logfile, "file.log"), + } + _, err := handleLoggingParams(ctx, cfg) + require.Error(t, err) + }) + t.Run("default", func(t *testing.T) { set := flag.NewFlagSet("flagSet", flag.ExitOnError) ctx := cli.NewContext(cli.NewApp(), set, nil) @@ -83,6 +98,12 @@ func TestInitBCWithMetrics(t *testing.T) { require.NoError(t, err) logger, err := handleLoggingParams(ctx, cfg.ApplicationConfiguration) require.NoError(t, err) + + t.Run("bad store", func(t *testing.T) { + _, _, _, err = initBCWithMetrics(config.Config{}, logger) + require.Error(t, err) + }) + chain, prometheus, pprof, err := initBCWithMetrics(cfg, logger) require.NoError(t, err) t.Cleanup(func() { @@ -139,9 +160,10 @@ func TestRestoreDB(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { require.NoError(t, os.Chdir(serverTestWD)) }) - //dump first + // dump first set := flag.NewFlagSet("flagSet", flag.ExitOnError) - set.String("config-path", filepath.Join(serverTestWD, "..", "..", "config"), "") + goodCfg := filepath.Join(serverTestWD, "..", "..", "config") + cfgPath := set.String("config-path", goodCfg, "") set.Bool("privnet", true, "") set.Bool("debug", true, "") set.Int("start", 0, "") @@ -152,7 +174,101 @@ func TestRestoreDB(t *testing.T) { require.NoError(t, err) // and then restore - set.String("in", testDump, "") + t.Run("invalid config", func(t *testing.T) { + *cfgPath = filepath.Join(serverTestWD, "..", "..", "config_invalid") + require.Error(t, restoreDB(ctx)) + }) + t.Run("invalid logger path", func(t *testing.T) { + badCfgDir := t.TempDir() + logfile := filepath.Join(badCfgDir, "logdir") + require.NoError(t, os.WriteFile(logfile, []byte{1, 2, 3}, os.ModePerm)) + cfg, err := config.LoadFile(filepath.Join(goodCfg, "protocol.privnet.yml")) + require.NoError(t, err, "could not load config") + cfg.ApplicationConfiguration.LogPath = filepath.Join(logfile, "file.log") + out, err := yaml.Marshal(cfg) + require.NoError(t, err) + + badCfgPath := filepath.Join(badCfgDir, "protocol.privnet.yml") + require.NoError(t, ioutil.WriteFile(badCfgPath, out, os.ModePerm)) + + *cfgPath = badCfgDir + require.Error(t, restoreDB(ctx)) + + *cfgPath = goodCfg + }) + t.Run("invalid bc config", func(t *testing.T) { + badCfgDir := t.TempDir() + cfg, err := config.LoadFile(filepath.Join(goodCfg, "protocol.privnet.yml")) + require.NoError(t, err, "could not load config") + cfg.ApplicationConfiguration.DBConfiguration.Type = "" + out, err := yaml.Marshal(cfg) + require.NoError(t, err) + + badCfgPath := filepath.Join(badCfgDir, "protocol.privnet.yml") + require.NoError(t, ioutil.WriteFile(badCfgPath, out, os.ModePerm)) + + *cfgPath = badCfgDir + require.Error(t, restoreDB(ctx)) + + *cfgPath = goodCfg + }) + + in := set.String("in", testDump, "") + incremental := set.Bool("incremental", false, "") + t.Run("invalid in", func(t *testing.T) { + *in = "unknown-file" + require.Error(t, restoreDB(ctx)) + + *in = testDump + }) + t.Run("corrupted in: invalid block count", func(t *testing.T) { + inPath := filepath.Join(t.TempDir(), "file3.acc") + require.NoError(t, os.WriteFile(inPath, []byte{1, 2, 3}, // file is expected to start from uint32 + os.ModePerm)) + *in = inPath + require.Error(t, restoreDB(ctx)) + + *in = testDump + }) + t.Run("corrupted in: corrupted block", func(t *testing.T) { + inPath := filepath.Join(t.TempDir(), "file3.acc") + b, err := os.ReadFile(testDump) + require.NoError(t, err) + b[5] = 0xff // file is expected to start from uint32 (4 bytes) followed by the first block, so corrupt the first block bytes + require.NoError(t, os.WriteFile(inPath, b, os.ModePerm)) + *in = inPath + require.Error(t, restoreDB(ctx)) + + *in = testDump + }) + t.Run("incremental dump", func(t *testing.T) { + inPath := filepath.Join(t.TempDir(), "file1_incremental.acc") + b, err := os.ReadFile(testDump) + require.NoError(t, err) + start := make([]byte, 4) + t.Run("good", func(t *testing.T) { + binary.LittleEndian.PutUint32(start, 1) // start from the first block + require.NoError(t, os.WriteFile(inPath, append(start, b...), + os.ModePerm)) + *in = inPath + *incremental = true + + require.NoError(t, restoreDB(ctx)) + }) + t.Run("dump is too high", func(t *testing.T) { + binary.LittleEndian.PutUint32(start, 2) // start from the second block + require.NoError(t, os.WriteFile(inPath, append(start, b...), + os.ModePerm)) + *in = inPath + *incremental = true + + require.Error(t, restoreDB(ctx)) + }) + + *in = testDump + *incremental = false + }) + set.String("dump", saveDump, "") require.NoError(t, restoreDB(ctx)) } diff --git a/cli/server_test.go b/cli/server_test.go new file mode 100644 index 000000000..200c3e7d8 --- /dev/null +++ b/cli/server_test.go @@ -0,0 +1,124 @@ +package main + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/nspcc-dev/neo-go/cli/server" + "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +func TestServerStart(t *testing.T) { + tmpDir := t.TempDir() + goodCfg, err := config.LoadFile(filepath.Join("..", "config", "protocol.unit_testnet.yml")) + require.NoError(t, err, "could not load config") + ptr := &goodCfg + saveCfg := func(t *testing.T, f func(cfg *config.Config)) string { + cfg := *ptr + chainPath := filepath.Join(t.TempDir(), "neogotestchain") + cfg.ApplicationConfiguration.DBConfiguration.Type = "leveldb" + cfg.ApplicationConfiguration.DBConfiguration.LevelDBOptions.DataDirectoryPath = chainPath + f(&cfg) + out, err := yaml.Marshal(cfg) + require.NoError(t, err) + + cfgPath := filepath.Join(tmpDir, "protocol.unit_testnet.yml") + require.NoError(t, ioutil.WriteFile(cfgPath, out, os.ModePerm)) + t.Cleanup(func() { + require.NoError(t, os.Remove(cfgPath)) + }) + return cfgPath + } + + baseCmd := []string{"neo-go", "node", "--unittest", "--config-path", tmpDir} + e := newExecutor(t, false) + + t.Run("invalid config path", func(t *testing.T) { + e.RunWithError(t, baseCmd...) + }) + t.Run("bad logger config", func(t *testing.T) { + badConfigDir := t.TempDir() + logfile := filepath.Join(badConfigDir, "logdir") + require.NoError(t, os.WriteFile(logfile, []byte{1, 2, 3}, os.ModePerm)) + saveCfg(t, func(cfg *config.Config) { + cfg.ApplicationConfiguration.LogPath = filepath.Join(logfile, "file.log") + }) + e.RunWithError(t, baseCmd...) + }) + t.Run("invalid storage", func(t *testing.T) { + saveCfg(t, func(cfg *config.Config) { + cfg.ApplicationConfiguration.DBConfiguration.Type = "" + }) + e.RunWithError(t, baseCmd...) + }) + t.Run("stateroot service is on && StateRootInHeader=true", func(t *testing.T) { + saveCfg(t, func(cfg *config.Config) { + cfg.ApplicationConfiguration.StateRoot.Enabled = true + cfg.ProtocolConfiguration.StateRootInHeader = true + }) + e.RunWithError(t, baseCmd...) + }) + t.Run("invalid Oracle config", func(t *testing.T) { + saveCfg(t, func(cfg *config.Config) { + cfg.ApplicationConfiguration.Oracle.Enabled = true + cfg.ApplicationConfiguration.Oracle.UnlockWallet.Path = "bad_orc_wallet.json" + }) + e.RunWithError(t, baseCmd...) + }) + t.Run("invalid consensus config", func(t *testing.T) { + saveCfg(t, func(cfg *config.Config) { + cfg.ApplicationConfiguration.UnlockWallet.Path = "bad_consensus_wallet.json" + }) + e.RunWithError(t, baseCmd...) + }) + t.Run("invalid Notary config", func(t *testing.T) { + t.Run("malformed config", func(t *testing.T) { + saveCfg(t, func(cfg *config.Config) { + cfg.ProtocolConfiguration.P2PSigExtensions = false + cfg.ApplicationConfiguration.P2PNotary.Enabled = true + }) + e.RunWithError(t, baseCmd...) + }) + t.Run("invalid wallet", func(t *testing.T) { + saveCfg(t, func(cfg *config.Config) { + cfg.ProtocolConfiguration.P2PSigExtensions = true + cfg.ApplicationConfiguration.P2PNotary.Enabled = true + cfg.ApplicationConfiguration.P2PNotary.UnlockWallet.Path = "bad_notary_wallet.json" + }) + e.RunWithError(t, baseCmd...) + }) + }) + t.Run("good", func(t *testing.T) { + saveCfg(t, func(cfg *config.Config) {}) + + go func() { + e.Run(t, baseCmd...) + }() + + var line string + require.Eventually(t, func() bool { + line, err = e.Out.ReadString('\n') + if err != nil && err != io.EOF { + t.Fatalf(fmt.Sprintf("unexpected error while reading CLI output: %s", err)) + } + return err == nil + }, 2*time.Second, 100*time.Millisecond) + lines := strings.Split(server.Logo(), "\n") + for _, expected := range lines { + // It should be regexp, so escape all backslashes. + expected = strings.ReplaceAll(expected, `\`, `\\`) + e.checkLine(t, line, expected) + line = e.getNextLine(t) + } + e.checkNextLine(t, "") + e.checkEOF(t) + }) +}