Skip to content

Commit

Permalink
refactor: Improve CLI with test suite and builder pattern (sourcenetw…
Browse files Browse the repository at this point in the history
  • Loading branch information
orpheuslummis authored Apr 25, 2023
1 parent 70c74d9 commit 7d2ce6e
Show file tree
Hide file tree
Showing 50 changed files with 2,302 additions and 1,120 deletions.
12 changes: 6 additions & 6 deletions cli/blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import (
"github.com/spf13/cobra"
)

var blocksCmd = &cobra.Command{
Use: "blocks",
Short: "Interact with the database's blockstore",
}
func MakeBlocksCommand() *cobra.Command {
var cmd = &cobra.Command{
Use: "blocks",
Short: "Interact with the database's blockstore",
}

func init() {
clientCmd.AddCommand(blocksCmd)
return cmd
}
92 changes: 46 additions & 46 deletions cli/blocks_get.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,63 +18,63 @@ import (
"github.com/spf13/cobra"

httpapi "github.com/sourcenetwork/defradb/api/http"
"github.com/sourcenetwork/defradb/config"
)

var getCmd = &cobra.Command{
Use: "get [CID]",
Short: "Get a block by its CID from the blockstore.",
RunE: func(cmd *cobra.Command, args []string) (err error) {
if len(args) != 1 {
return NewErrMissingArg("CID")
}
cid := args[0]

endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.BlocksPath, cid)
if err != nil {
return NewErrFailedToJoinEndpoint(err)
}
func MakeBlocksGetCommand(cfg *config.Config) *cobra.Command {
var cmd = &cobra.Command{
Use: "get [CID]",
Short: "Get a block by its CID from the blockstore.",
RunE: func(cmd *cobra.Command, args []string) (err error) {
if len(args) != 1 {
return NewErrMissingArg("CID")
}
cid := args[0]

res, err := http.Get(endpoint.String())
if err != nil {
return NewErrFailedToSendRequest(err)
}
endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.BlocksPath, cid)
if err != nil {
return NewErrFailedToJoinEndpoint(err)
}

defer func() {
if e := res.Body.Close(); e != nil {
err = NewErrFailedToReadResponseBody(err)
res, err := http.Get(endpoint.String())
if err != nil {
return NewErrFailedToSendRequest(err)
}
}()

response, err := io.ReadAll(res.Body)
if err != nil {
return NewErrFailedToReadResponseBody(err)
}
defer func() {
if e := res.Body.Close(); e != nil {
err = NewErrFailedToReadResponseBody(err)
}
}()

stdout, err := os.Stdout.Stat()
if err != nil {
return NewErrFailedToStatStdOut(err)
}
if isFileInfoPipe(stdout) {
cmd.Println(string(response))
} else {
graphlErr, err := hasGraphQLErrors(response)
response, err := io.ReadAll(res.Body)
if err != nil {
return NewErrFailedToHandleGQLErrors(err)
return NewErrFailedToReadResponseBody(err)
}
indentedResult, err := indentJSON(response)

stdout, err := os.Stdout.Stat()
if err != nil {
return NewErrFailedToPrettyPrintResponse(err)
return NewErrFailedToStatStdOut(err)
}
if graphlErr {
log.FeedbackError(cmd.Context(), indentedResult)
if isFileInfoPipe(stdout) {
cmd.Println(string(response))
} else {
log.FeedbackInfo(cmd.Context(), indentedResult)
graphlErr, err := hasGraphQLErrors(response)
if err != nil {
return NewErrFailedToHandleGQLErrors(err)
}
indentedResult, err := indentJSON(response)
if err != nil {
return NewErrFailedToPrettyPrintResponse(err)
}
if graphlErr {
log.FeedbackError(cmd.Context(), indentedResult)
} else {
log.FeedbackInfo(cmd.Context(), indentedResult)
}
}
}
return nil
},
}

func init() {
blocksCmd.AddCommand(getCmd)
return nil
},
}
return cmd
}
83 changes: 72 additions & 11 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ import (
"os"
"strings"

"github.com/spf13/cobra"

"github.com/sourcenetwork/defradb/config"
"github.com/sourcenetwork/defradb/errors"
"github.com/sourcenetwork/defradb/logging"
)

var log = logging.MustNewLogger("defra.cli")

const badgerDatastoreName = "badger"

// Errors with how the command is invoked by user
Expand All @@ -42,30 +46,87 @@ var usageErrors = []string{
errTooManyArgs,
}

var log = logging.MustNewLogger("defra.cli")
type DefraCommand struct {
RootCmd *cobra.Command
Cfg *config.Config
}

// NewDefraCommand returns the root command instanciated with its tree of subcommands.
func NewDefraCommand(cfg *config.Config) DefraCommand {
rootCmd := MakeRootCommand(cfg)
rpcCmd := MakeRPCCommand(cfg)
blocksCmd := MakeBlocksCommand()
schemaCmd := MakeSchemaCommand()
clientCmd := MakeClientCommand()
rpcReplicatorCmd := MakeReplicatorCommand()
p2pCollectionCmd := MakeP2PCollectionCommand()
p2pCollectionCmd.AddCommand(
MakeP2PCollectionAddCommand(cfg),
MakeP2PCollectionRemoveCommand(cfg),
MakeP2PCollectionGetallCommand(cfg),
)
rpcReplicatorCmd.AddCommand(
MakeReplicatorGetallCommand(cfg),
MakeReplicatorSetCommand(cfg),
MakeReplicatorDeleteCommand(cfg),
)
rpcCmd.AddCommand(
rpcReplicatorCmd,
p2pCollectionCmd,
)
blocksCmd.AddCommand(
MakeBlocksGetCommand(cfg),
)
schemaCmd.AddCommand(
MakeSchemaAddCommand(cfg),
MakeSchemaPatchCommand(cfg),
)
clientCmd.AddCommand(
MakeDumpCommand(cfg),
MakePingCommand(cfg),
MakeRequestCommand(cfg),
MakePeerIDCommand(cfg),
schemaCmd,
rpcCmd,
blocksCmd,
)
rootCmd.AddCommand(
clientCmd,
MakeStartCommand(cfg),
MakeServerDumpCmd(cfg),
MakeVersionCommand(),
MakeInitCommand(cfg),
)

var cfg = config.DefaultConfig()
var RootCmd = rootCmd
return DefraCommand{rootCmd, cfg}
}

func Execute() {
ctx := context.Background()
func (defraCmd *DefraCommand) Execute(ctx context.Context) error {
// Silence cobra's default output to control usage and error display.
rootCmd.SilenceUsage = true
rootCmd.SilenceErrors = true
rootCmd.SetOut(os.Stdout)
cmd, err := rootCmd.ExecuteContextC(ctx)
defraCmd.RootCmd.SilenceUsage = true
defraCmd.RootCmd.SilenceErrors = true
defraCmd.RootCmd.SetOut(os.Stdout)
cmd, err := defraCmd.RootCmd.ExecuteContextC(ctx)
if err != nil {
// Intentional cancellation.
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return nil
}
// User error.
for _, cobraError := range usageErrors {
if strings.HasPrefix(err.Error(), cobraError) {
log.FeedbackErrorE(ctx, "Usage error", err)
if usageErr := cmd.Usage(); usageErr != nil {
log.FeedbackFatalE(ctx, "error displaying usage help", usageErr)
}
os.Exit(1)
return err
}
}
log.FeedbackFatalE(ctx, "Execution error", err)
// Internal error.
log.FeedbackErrorE(ctx, "Execution error", err)
return err
}
return nil
}

func isFileInfoPipe(fi os.FileInfo) bool {
Expand Down
84 changes: 84 additions & 0 deletions cli/cli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright 2022 Democratized Data Foundation
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package cli

import (
"fmt"
"math/rand"
"net"
"testing"

"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"

"github.com/sourcenetwork/defradb/config"
"github.com/sourcenetwork/defradb/errors"
)

// Verify that the top-level commands are registered, and if particular ones have subcommands.
func TestNewDefraCommand(t *testing.T) {
expectedCommandNames := []string{
"client",
"init",
"server-dump",
"start",
"version",
}
actualCommandNames := []string{}
r := NewDefraCommand(config.DefaultConfig())
for _, c := range r.RootCmd.Commands() {
actualCommandNames = append(actualCommandNames, c.Name())
}
for _, expectedCommandName := range expectedCommandNames {
assert.Contains(t, actualCommandNames, expectedCommandName)
}
for _, c := range r.RootCmd.Commands() {
if c.Name() == "client" {
assert.NotEmpty(t, c.Commands())
}
}
}

func TestAllHaveUsage(t *testing.T) {
cfg := config.DefaultConfig()
defra := NewDefraCommand(cfg)
walkCommandTree(t, defra.RootCmd, func(c *cobra.Command) {
assert.NotEmpty(t, c.Use)
})
}

func walkCommandTree(t *testing.T, cmd *cobra.Command, f func(*cobra.Command)) {
f(cmd)
for _, c := range cmd.Commands() {
walkCommandTree(t, c, f)
}
}

// findFreePortInRange returns a free port in the range [minPort, maxPort].
// The range of ports that are unfrequently used is [49152, 65535].
func findFreePortInRange(minPort, maxPort int) (int, error) {
if minPort < 1 || maxPort > 65535 || minPort > maxPort {
return 0, errors.New("invalid port range")
}

const maxAttempts = 100
for i := 0; i < maxAttempts; i++ {
port := rand.Intn(maxPort-minPort+1) + minPort
addr := fmt.Sprintf("127.0.0.1:%d", port)
listener, err := net.Listen("tcp", addr)
if err == nil {
_ = listener.Close()
return port, nil
}
}

return 0, errors.New("unable to find a free port")
}
14 changes: 7 additions & 7 deletions cli/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ import (
"github.com/spf13/cobra"
)

var clientCmd = &cobra.Command{
Use: "client",
Short: "Interact with a running DefraDB node as a client",
Long: `Interact with a running DefraDB node as a client.
func MakeClientCommand() *cobra.Command {
var cmd = &cobra.Command{
Use: "client",
Short: "Interact with a running DefraDB node as a client",
Long: `Interact with a running DefraDB node as a client.
Execute queries, add schema types, and run debug routines.`,
}
}

func init() {
rootCmd.AddCommand(clientCmd)
return cmd
}
Loading

0 comments on commit 7d2ce6e

Please sign in to comment.