From b9f1d677416aeccba92ae39c6e255a5ec94d0c0d Mon Sep 17 00:00:00 2001 From: ktr0731 Date: Sun, 10 Nov 2019 14:38:46 +0900 Subject: [PATCH 01/12] migrate to cobra --- app/app.go | 120 ++++---------------------------- app/commands.go | 179 ++++++++++++++++++++++++++++++++++++++++++++++++ app/flag.go | 80 ---------------------- go.mod | 1 + go.sum | 13 +++- 5 files changed, 206 insertions(+), 187 deletions(-) create mode 100644 app/commands.go diff --git a/app/app.go b/app/app.go index 15514675..d07a7447 100644 --- a/app/app.go +++ b/app/app.go @@ -2,49 +2,45 @@ package app import ( - "context" "fmt" - "os" + "io" - "github.com/ktr0731/evans/cache" "github.com/ktr0731/evans/config" "github.com/ktr0731/evans/cui" - "github.com/ktr0731/evans/logger" "github.com/ktr0731/evans/meta" - "github.com/ktr0731/evans/mode" - "github.com/ktr0731/evans/prompt" "github.com/pkg/errors" + "github.com/spf13/cobra" "github.com/spf13/pflag" - "golang.org/x/sync/errgroup" ) // App is the root component for running the application. type App struct { cui cui.UI - - flagSet *pflag.FlagSet - - cfg *mergedConfig + cmd *cobra.Command } // New instantiates a new App instance. ui must not be a nil. // Note that cui is also used for the REPL UI if the mode is REPL mode. func New(ui cui.UI) *App { + var flags flags + cmd := newCommand(&flags, ui) return &App{ cui: ui, + cmd: cmd, } } // Run starts the application. The return value means the exit code. func (a *App) Run(args []string) int { - err := a.run(args) + a.cmd.SetArgs(args) + err := a.cmd.Execute() if err == nil { return 0 } switch err := err.(type) { case *config.ValidationError: - a.printUsage() + printUsage(a.cmd) a.cui.Error(fmt.Sprintf("evans: %s", err.Err)) default: a.cui.Error(fmt.Sprintf("evans: %s", err)) @@ -52,99 +48,13 @@ func (a *App) Run(args []string) int { return 1 } -func (a *App) run(args []string) error { - flags, err := a.parseFlags(args) - if err != nil { - return err - } - if err := flags.validate(); err != nil { - return errors.Wrap(err, "invalid flag condition") - } - - if flags.meta.verbose { - logger.SetOutput(os.Stderr) - } - - switch { - case flags.meta.edit: - if err := config.Edit(); err != nil { - return errors.Wrap(err, "failed to edit the project local config file") - } - return nil - case flags.meta.editGlobal: - if err := config.EditGlobal(); err != nil { - return errors.Wrap(err, "failed to edit the global config file") - } - return nil - case flags.meta.version: - a.printVersion() - return nil - case flags.meta.help: - a.printUsage() - return nil - } - - cfg, err := mergeConfig(a.flagSet, flags) - if err != nil { - if err, ok := err.(*config.ValidationError); ok { - return err - } - return errors.Wrap(err, "failed to merge command line flags and config files") - } - a.cfg = cfg - - if cfg.REPL.ColoredOutput { - a.cui = cui.NewColored(a.cui) - } - - isCLIMode := (cfg.cli || mode.IsCLIMode(cfg.file)) - if cfg.repl || !isCLIMode { - cache, err := cache.Get() - if err != nil { - return errors.Wrap(err, "failed to get the cache content") - } - - baseCtx, cancel := context.WithCancel(context.Background()) - defer cancel() - eg, ctx := errgroup.WithContext(baseCtx) - // Run update checker asynchronously. - eg.Go(func() error { - return checkUpdate(ctx, a.cfg.Config, cache) - }) - - if a.cfg.Config.Meta.AutoUpdate { - eg.Go(func() error { - return processUpdate(ctx, a.cfg.Config, a.cui.Writer(), cache, prompt.New()) - }) - } else if err := processUpdate(ctx, a.cfg.Config, a.cui.Writer(), cache, prompt.New()); err != nil { - return errors.Wrap(err, "failed to update Evans") - } - - if err := mode.RunAsREPLMode(a.cfg.Config, a.cui, cache); err != nil { - return errors.Wrap(err, "failed to run REPL mode") - } - - // Always call cancel func because it is hope to abort update checking if REPL mode is finished - // before update checking. If update checking is finished before REPL mode, cancel do nothing. - cancel() - if err := eg.Wait(); err != nil { - return errors.Wrap(err, "failed to check application update") - } - } else if err := mode.RunAsCLIMode(a.cfg.Config, a.cfg.call, a.cfg.file, a.cui); err != nil { - return errors.Wrap(err, "failed to run CLI mode") - } - - return nil -} - // printUsage shows the command usage text to cui.Writer and exit. Do not call it before calling parseFlags. -func (a *App) printUsage() { - a.flagSet.Usage() +func printUsage(cmd *cobra.Command) { + _ = cmd.Help() // Help never return errors. } -// printVersion shows the version of the command and exit. -func (a *App) printVersion() { - a.cui.Output(fmt.Sprintf("%s %s", meta.AppName, meta.Version.String())) +func printVersion(w io.Writer) { + fmt.Fprintf(w, "%s %s\n", meta.AppName, meta.Version.String()) } // mergedConfig represents the conclusive config. Common config items are stored to *config.Config. @@ -163,12 +73,12 @@ type mergedConfig struct { repl bool } -func mergeConfig(fs *pflag.FlagSet, flags *flags) (*mergedConfig, error) { +func mergeConfig(fs *pflag.FlagSet, flags *flags, args []string) (*mergedConfig, error) { cfg, err := config.Get(fs) if err != nil { return nil, errors.Wrap(err, "failed to get config") } - protos := fs.Args() + protos := args cfg.Default.ProtoFile = append(cfg.Default.ProtoFile, protos...) if err := cfg.Validate(); err != nil { diff --git a/app/commands.go b/app/commands.go new file mode 100644 index 00000000..411e3957 --- /dev/null +++ b/app/commands.go @@ -0,0 +1,179 @@ +package app + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "text/tabwriter" + + "github.com/ktr0731/evans/cache" + "github.com/ktr0731/evans/config" + "github.com/ktr0731/evans/cui" + "github.com/ktr0731/evans/logger" + "github.com/ktr0731/evans/meta" + "github.com/ktr0731/evans/mode" + "github.com/ktr0731/evans/prompt" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "golang.org/x/sync/errgroup" +) + +func newCommand(flags *flags, ui cui.UI) *cobra.Command { + cmd := &cobra.Command{ + Short: "root command", + RunE: func(cmd *cobra.Command, args []string) error { + if err := flags.validate(); err != nil { + return errors.Wrap(err, "invalid flag condition") + } + + if flags.meta.verbose { + logger.SetOutput(os.Stderr) + } + + switch { + case flags.meta.edit: + if err := config.Edit(); err != nil { + return errors.Wrap(err, "failed to edit the project local config file") + } + return nil + case flags.meta.editGlobal: + if err := config.EditGlobal(); err != nil { + return errors.Wrap(err, "failed to edit the global config file") + } + return nil + case flags.meta.version: + printVersion(ui.Writer()) + return nil + case flags.meta.help: + printUsage(cmd) + return nil + } + + cfg, err := mergeConfig(cmd.PersistentFlags(), flags, args) + if err != nil { + if err, ok := err.(*config.ValidationError); ok { + return err + } + return errors.Wrap(err, "failed to merge command line flags and config files") + } + + if cfg.REPL.ColoredOutput { + ui = cui.NewColored(ui) + } + + isCLIMode := (cfg.cli || mode.IsCLIMode(cfg.file)) + if cfg.repl || !isCLIMode { + cache, err := cache.Get() + if err != nil { + return errors.Wrap(err, "failed to get the cache content") + } + + baseCtx, cancel := context.WithCancel(context.Background()) + defer cancel() + eg, ctx := errgroup.WithContext(baseCtx) + // Run update checker asynchronously. + eg.Go(func() error { + return checkUpdate(ctx, cfg.Config, cache) + }) + + if cfg.Config.Meta.AutoUpdate { + eg.Go(func() error { + return processUpdate(ctx, cfg.Config, ui.Writer(), cache, prompt.New()) + }) + } else if err := processUpdate(ctx, cfg.Config, ui.Writer(), cache, prompt.New()); err != nil { + return errors.Wrap(err, "failed to update Evans") + } + + if err := mode.RunAsREPLMode(cfg.Config, ui, cache); err != nil { + return errors.Wrap(err, "failed to run REPL mode") + } + + // Always call cancel func because it is hope to abort update checking if REPL mode is finished + // before update checking. If update checking is finished before REPL mode, cancel do nothing. + cancel() + if err := eg.Wait(); err != nil { + return errors.Wrap(err, "failed to check application update") + } + } else if err := mode.RunAsCLIMode(cfg.Config, cfg.call, cfg.file, ui); err != nil { + return errors.Wrap(err, "failed to run CLI mode") + } + + return nil + }, + SilenceErrors: true, + SilenceUsage: true, + } + bindFlags(cmd.PersistentFlags(), flags, ui.Writer()) + cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { + cmd.PersistentFlags().Usage() + }) + return cmd +} + +func bindFlags(f *pflag.FlagSet, flags *flags, w io.Writer) { + f.SortFlags = false + f.SetOutput(w) + + f.BoolVar(&flags.mode.repl, "repl", false, "launch Evans as REPL mode") + f.BoolVar(&flags.mode.cli, "cli", false, "start as CLI mode") + + f.StringVar(&flags.cli.call, "call", "", "call specified RPC by CLI mode") + f.StringVarP(&flags.cli.file, "file", "f", "", "a script file that will be executed by (used only CLI mode)") + + f.BoolVarP(&flags.repl.silent, "silent", "s", false, "hide redundant output") + + f.StringVar(&flags.common.pkg, "package", "", "default package") + f.StringVar(&flags.common.service, "service", "", "default service") + f.StringSliceVar(&flags.common.path, "path", nil, "proto file paths") + f.StringVar(&flags.common.host, "host", "", "gRPC server host") + f.StringVarP(&flags.common.port, "port", "p", "50051", "gRPC server port") + f.Var( + newStringToStringValue(nil, &flags.common.header), + "header", "default headers that set to each requests (example: foo=bar)") + f.BoolVar(&flags.common.web, "web", false, "use gRPC-Web protocol") + f.BoolVarP(&flags.common.reflection, "reflection", "r", false, "use gRPC reflection") + f.BoolVarP(&flags.common.tls, "tls", "t", false, "use a secure TLS connection") + f.StringVar(&flags.common.cacert, "cacert", "", "the CA certificate file for verifying the server") + f.StringVar( + &flags.common.cert, + "cert", "", "the certificate file for mutual TLS auth. it must be provided with --certkey.") + f.StringVar( + &flags.common.certKey, + "certkey", "", "the private key file for mutual TLS auth. it must be provided with --cert.") + f.StringVar( + &flags.common.serverName, + "servername", "", "override the server name used to verify the hostname (ignored if --tls is disabled)") + + f.BoolVarP(&flags.meta.edit, "edit", "e", false, "edit the project config file by using $EDITOR") + f.BoolVar(&flags.meta.editGlobal, "edit-global", false, "edit the global config file by using $EDITOR") + f.BoolVar(&flags.meta.verbose, "verbose", false, "verbose output") + f.BoolVarP(&flags.meta.version, "version", "v", false, "display version and exit") + f.BoolVarP(&flags.meta.help, "help", "h", false, "display help text and exit") + + f.Usage = func() { + out := w + printVersion(out) + var buf bytes.Buffer + w := tabwriter.NewWriter(&buf, 0, 8, 8, ' ', tabwriter.TabIndent) + f.VisitAll(func(f *pflag.Flag) { + cmd := "--" + f.Name + if f.Shorthand != "" { + cmd += ", -" + f.Shorthand + } + name, _ := pflag.UnquoteUsage(f) + if name != "" { + cmd += " " + name + } + usage := f.Usage + if f.DefValue != "" { + usage += fmt.Sprintf(` (default "%s")`, f.DefValue) + } + fmt.Fprintf(w, " %s\t%s\n", cmd, usage) + }) + w.Flush() + fmt.Fprintf(out, usageFormat, meta.AppName, buf.String()) + } +} diff --git a/app/flag.go b/app/flag.go index de286798..d56a5688 100644 --- a/app/flag.go +++ b/app/flag.go @@ -5,12 +5,9 @@ import ( "encoding/csv" "fmt" "strings" - "text/tabwriter" - "github.com/ktr0731/evans/meta" "github.com/ktr0731/go-multierror" "github.com/pkg/errors" - "github.com/spf13/pflag" ) var usageFormat = ` @@ -81,83 +78,6 @@ func (f *flags) validate() error { return result } -func (a *App) parseFlags(args []string) (*flags, error) { - f := pflag.NewFlagSet("main", pflag.ContinueOnError) - f.SortFlags = false - f.SetOutput(a.cui.Writer()) - - var flags flags - - f.BoolVar(&flags.mode.repl, "repl", false, "launch Evans as REPL mode") - f.BoolVar(&flags.mode.cli, "cli", false, "start as CLI mode") - - f.StringVar(&flags.cli.call, "call", "", "call specified RPC by CLI mode") - f.StringVarP(&flags.cli.file, "file", "f", "", "a script file that will be executed by (used only CLI mode)") - - f.BoolVarP(&flags.repl.silent, "silent", "s", false, "hide redundant output") - - f.StringVar(&flags.common.pkg, "package", "", "default package") - f.StringVar(&flags.common.service, "service", "", "default service") - f.StringSliceVar(&flags.common.path, "path", nil, "proto file paths") - f.StringVar(&flags.common.host, "host", "", "gRPC server host") - f.StringVarP(&flags.common.port, "port", "p", "50051", "gRPC server port") - f.Var( - newStringToStringValue(nil, &flags.common.header), - "header", "default headers that set to each requests (example: foo=bar)") - f.BoolVar(&flags.common.web, "web", false, "use gRPC-Web protocol") - f.BoolVarP(&flags.common.reflection, "reflection", "r", false, "use gRPC reflection") - f.BoolVarP(&flags.common.tls, "tls", "t", false, "use a secure TLS connection") - f.StringVar(&flags.common.cacert, "cacert", "", "the CA certificate file for verifying the server") - f.StringVar( - &flags.common.cert, - "cert", "", "the certificate file for mutual TLS auth. it must be provided with --certkey.") - f.StringVar( - &flags.common.certKey, - "certkey", "", "the private key file for mutual TLS auth. it must be provided with --cert.") - f.StringVar( - &flags.common.serverName, - "servername", "", "override the server name used to verify the hostname (ignored if --tls is disabled)") - - f.BoolVarP(&flags.meta.edit, "edit", "e", false, "edit the project config file by using $EDITOR") - f.BoolVar(&flags.meta.editGlobal, "edit-global", false, "edit the global config file by using $EDITOR") - f.BoolVar(&flags.meta.verbose, "verbose", false, "verbose output") - f.BoolVarP(&flags.meta.version, "version", "v", false, "display version and exit") - f.BoolVarP(&flags.meta.help, "help", "h", false, "display help text and exit") - - f.Usage = func() { - a.printVersion() - var buf bytes.Buffer - w := tabwriter.NewWriter(&buf, 0, 8, 8, ' ', tabwriter.TabIndent) - f.VisitAll(func(f *pflag.Flag) { - cmd := "--" + f.Name - if f.Shorthand != "" { - cmd += ", -" + f.Shorthand - } - name, _ := pflag.UnquoteUsage(f) - if name != "" { - cmd += " " + name - } - usage := f.Usage - if f.DefValue != "" { - usage += fmt.Sprintf(` (default "%s")`, f.DefValue) - } - fmt.Fprintf(w, " %s\t%s\n", cmd, usage) - }) - w.Flush() - fmt.Fprintf(a.cui.Writer(), usageFormat, meta.AppName, buf.String()) - } - - // ignore error because flag set mode is ExitOnError - err := f.Parse(args) - if err != nil { - return nil, err - } - - a.flagSet = f - - return &flags, nil -} - // -- stringToString Value type stringToStringSliceValue struct { value *map[string][]string diff --git a/go.mod b/go.mod index 0f25fdaf..4d1ef60e 100644 --- a/go.mod +++ b/go.mod @@ -41,6 +41,7 @@ require ( github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 // indirect github.com/rakyll/statik v0.1.6 github.com/spf13/afero v1.2.2 // indirect + github.com/spf13/cobra v0.0.5 github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.4.0 diff --git a/go.sum b/go.sum index 03249c91..7f1415c1 100644 --- a/go.sum +++ b/go.sum @@ -32,9 +32,11 @@ github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -99,9 +101,9 @@ github.com/improbable-eng/grpc-web v0.9.1 h1:tenDg9Lg+zYXeS/ojbKyfwVO5TVYh5FFGsr github.com/improbable-eng/grpc-web v0.9.1/go.mod h1:6hRR09jOEG81ADP5wCQju1z71g6OL4eEvELdran/3cs= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jhump/protoreflect v0.0.0-20180803214909-95c5cbbeaee7/go.mod h1:eki7DI0mJrpRlDikO63gRZIG0keYYxgKDUdI2QmkZCQ= -github.com/jhump/protoreflect v1.5.0 h1:NgpVT+dX71c8hZnxHof2M7QDK7QtohIJ7DYycjnkyfc= -github.com/jhump/protoreflect v1.5.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/jhump/protoreflect v1.5.1-0.20191024213132-10815c273d3f h1:/3LGRkPUNMBo1CxyZ9+AkXGXvPagj2QS/owPN0VlagA= github.com/jhump/protoreflect v1.5.1-0.20191024213132-10815c273d3f/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= @@ -218,6 +220,7 @@ github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -226,6 +229,8 @@ github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= @@ -234,6 +239,7 @@ github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -249,6 +255,7 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1 github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9 h1:vY5WqiEon0ZSTGM3ayVVi+twaHKHDFUVloaQ/wug9/c= github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9/go.mod h1:q+QjxYvZ+fpjMXqs+XEriussHjSYqeXVnAdSV1tkMYk= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/zchee/go-xdgbasedir v1.0.3 h1:loLl3qosOHcMSCtV9ciISdjEQuXcj56BYccRNBvQKDY= @@ -260,6 +267,7 @@ go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -301,6 +309,7 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116161606-93218def8b18/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= From 533cb797333f44d4701fef78699dc577394dc4f7 Mon Sep 17 00:00:00 2001 From: ktr0731 Date: Sun, 15 Dec 2019 23:08:04 +0900 Subject: [PATCH 02/12] rename newCommand to newOldCommand --- app/app.go | 2 +- app/commands.go | 130 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 129 insertions(+), 3 deletions(-) diff --git a/app/app.go b/app/app.go index d07a7447..4360bbd5 100644 --- a/app/app.go +++ b/app/app.go @@ -23,7 +23,7 @@ type App struct { // Note that cui is also used for the REPL UI if the mode is REPL mode. func New(ui cui.UI) *App { var flags flags - cmd := newCommand(&flags, ui) + cmd := newOldCommand(&flags, ui) return &App{ cui: ui, cmd: cmd, diff --git a/app/commands.go b/app/commands.go index 411e3957..d5a39f9c 100644 --- a/app/commands.go +++ b/app/commands.go @@ -21,9 +21,8 @@ import ( "golang.org/x/sync/errgroup" ) -func newCommand(flags *flags, ui cui.UI) *cobra.Command { +func newOldCommand(flags *flags, ui cui.UI) *cobra.Command { cmd := &cobra.Command{ - Short: "root command", RunE: func(cmd *cobra.Command, args []string) error { if err := flags.validate(); err != nil { return errors.Wrap(err, "invalid flag condition") @@ -110,6 +109,8 @@ func newCommand(flags *flags, ui cui.UI) *cobra.Command { cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { cmd.PersistentFlags().Usage() }) + // TODO: Register sub-commands when old style command is failed. + // cmd.AddCommand(newCLICommand(flags, ui)) return cmd } @@ -177,3 +178,128 @@ func bindFlags(f *pflag.FlagSet, flags *flags, w io.Writer) { fmt.Fprintf(out, usageFormat, meta.AppName, buf.String()) } } + +func newCLICommand(flags *flags, ui cui.UI) *cobra.Command { + cmd := &cobra.Command{ + Use: "cli", + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Println("CLI MODE!") + // if err := flags.validate(); err != nil { + // return errors.Wrap(err, "invalid flag condition") + // } + // + // if flags.meta.verbose { + // logger.SetOutput(os.Stderr) + // } + // + // switch { + // case flags.meta.edit: + // if err := config.Edit(); err != nil { + // return errors.Wrap(err, "failed to edit the project local config file") + // } + // return nil + // case flags.meta.editGlobal: + // if err := config.EditGlobal(); err != nil { + // return errors.Wrap(err, "failed to edit the global config file") + // } + // return nil + // case flags.meta.version: + // printVersion(ui.Writer()) + // return nil + // case flags.meta.help: + // printUsage(cmd) + // return nil + // } + // + // cfg, err := mergeConfig(cmd.PersistentFlags(), flags, args) + // if err != nil { + // if err, ok := err.(*config.ValidationError); ok { + // return err + // } + // return errors.Wrap(err, "failed to merge command line flags and config files") + // } + // + // if cfg.REPL.ColoredOutput { + // ui = cui.NewColored(ui) + // } + // + // isCLIMode := (cfg.cli || mode.IsCLIMode(cfg.file)) + // if cfg.repl || !isCLIMode { + // cache, err := cache.Get() + // if err != nil { + // return errors.Wrap(err, "failed to get the cache content") + // } + // + // baseCtx, cancel := context.WithCancel(context.Background()) + // defer cancel() + // eg, ctx := errgroup.WithContext(baseCtx) + // // Run update checker asynchronously. + // eg.Go(func() error { + // return checkUpdate(ctx, cfg.Config, cache) + // }) + // + // if cfg.Config.Meta.AutoUpdate { + // eg.Go(func() error { + // return processUpdate(ctx, cfg.Config, ui.Writer(), cache, prompt.New()) + // }) + // } else if err := processUpdate(ctx, cfg.Config, ui.Writer(), cache, prompt.New()); err != nil { + // return errors.Wrap(err, "failed to update Evans") + // } + // + // if err := mode.RunAsREPLMode(cfg.Config, ui, cache); err != nil { + // return errors.Wrap(err, "failed to run REPL mode") + // } + // + // // Always call cancel func because it is hope to abort update checking if REPL mode is finished + // // before update checking. If update checking is finished before REPL mode, cancel do nothing. + // cancel() + // if err := eg.Wait(); err != nil { + // return errors.Wrap(err, "failed to check application update") + // } + // } else if err := mode.RunAsCLIMode(cfg.Config, cfg.call, cfg.file, ui); err != nil { + // return errors.Wrap(err, "failed to run CLI mode") + // } + + return nil + }, + SilenceErrors: true, + SilenceUsage: true, + } + bindCLIFlags(cmd.PersistentFlags(), flags, ui.Writer()) + cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { + cmd.PersistentFlags().Usage() + }) + return cmd +} + +func bindCLIFlags(f *pflag.FlagSet, flags *flags, w io.Writer) { + f.SortFlags = false + f.SetOutput(w) + + f.StringVar(&flags.cli.call, "call", "", "call specified RPC by CLI mode") + f.StringVarP(&flags.cli.file, "file", "f", "", "a script file that will be executed by (used only CLI mode)") + + f.Usage = func() { + out := w + printVersion(out) + var buf bytes.Buffer + w := tabwriter.NewWriter(&buf, 0, 8, 8, ' ', tabwriter.TabIndent) + f.VisitAll(func(f *pflag.Flag) { + cmd := "--" + f.Name + if f.Shorthand != "" { + cmd += ", -" + f.Shorthand + } + name, _ := pflag.UnquoteUsage(f) + if name != "" { + cmd += " " + name + } + usage := f.Usage + if f.DefValue != "" { + usage += fmt.Sprintf(` (default "%s")`, f.DefValue) + } + fmt.Fprintf(w, " %s\t%s\n", cmd, usage) + }) + w.Flush() + fmt.Fprintf(out, usageFormat, meta.AppName, buf.String()) + } +} From 41a65a75a42b654a569cb124f0e227b0fb1a2645 Mon Sep 17 00:00:00 2001 From: ktr0731 Date: Sun, 15 Dec 2019 23:49:57 +0900 Subject: [PATCH 03/12] integrate with new-style interface --- app/app.go | 17 ++++++++++++++--- app/commands.go | 18 ++++++++++++++---- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/app/app.go b/app/app.go index 4360bbd5..99248b61 100644 --- a/app/app.go +++ b/app/app.go @@ -9,14 +9,13 @@ import ( "github.com/ktr0731/evans/cui" "github.com/ktr0731/evans/meta" "github.com/pkg/errors" - "github.com/spf13/cobra" "github.com/spf13/pflag" ) // App is the root component for running the application. type App struct { cui cui.UI - cmd *cobra.Command + cmd *command } // New instantiates a new App instance. ui must not be a nil. @@ -33,6 +32,13 @@ func New(ui cui.UI) *App { // Run starts the application. The return value means the exit code. func (a *App) Run(args []string) int { a.cmd.SetArgs(args) + for _, r := range args { + if r == "-h" || r == "--help" { + // Hack. + // If the help flags is passed, call registerNewCommands for display sub-command helps. + a.cmd.registerNewCommands() + } + } err := a.cmd.Execute() if err == nil { return 0 @@ -43,13 +49,18 @@ func (a *App) Run(args []string) int { printUsage(a.cmd) a.cui.Error(fmt.Sprintf("evans: %s", err.Err)) default: + // Fallback to new-style interface. + a.cmd.registerNewCommands() + if err := a.cmd.Execute(); err == nil { + return 0 + } a.cui.Error(fmt.Sprintf("evans: %s", err)) } return 1 } // printUsage shows the command usage text to cui.Writer and exit. Do not call it before calling parseFlags. -func printUsage(cmd *cobra.Command) { +func printUsage(cmd interface{ Help() error }) { _ = cmd.Help() // Help never return errors. } diff --git a/app/commands.go b/app/commands.go index d5a39f9c..6f40b627 100644 --- a/app/commands.go +++ b/app/commands.go @@ -21,7 +21,19 @@ import ( "golang.org/x/sync/errgroup" ) -func newOldCommand(flags *flags, ui cui.UI) *cobra.Command { +type command struct { + *cobra.Command + + flags *flags + ui cui.UI +} + +// registerNewCommands registers sub-commands for new-style interface. +func (c *command) registerNewCommands() { + c.AddCommand(newCLICommand(c.flags, c.ui)) +} + +func newOldCommand(flags *flags, ui cui.UI) *command { cmd := &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { if err := flags.validate(); err != nil { @@ -109,9 +121,7 @@ func newOldCommand(flags *flags, ui cui.UI) *cobra.Command { cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { cmd.PersistentFlags().Usage() }) - // TODO: Register sub-commands when old style command is failed. - // cmd.AddCommand(newCLICommand(flags, ui)) - return cmd + return &command{cmd, flags, ui} } func bindFlags(f *pflag.FlagSet, flags *flags, w io.Writer) { From fe85c2ebeb656161c389f5b0ecea5a9bff87eb4c Mon Sep 17 00:00:00 2001 From: ktr0731 Date: Sun, 15 Dec 2019 23:54:05 +0900 Subject: [PATCH 04/12] add comments --- app/app.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/app.go b/app/app.go index 99248b61..0ca63788 100644 --- a/app/app.go +++ b/app/app.go @@ -31,6 +31,11 @@ func New(ui cui.UI) *App { // Run starts the application. The return value means the exit code. func (a *App) Run(args []string) int { + // Currently, Evans is migrating to new-style command-line interface. + // So, there are both of old-style and new-style command-line interfaces in this version. + // At first, Evans uses old-style for backward-compatibility, but if it fails, Evans fallbacks to new-style. + // See https://github.com/ktr0731/evans/issues/190 for more details. + a.cmd.SetArgs(args) for _, r := range args { if r == "-h" || r == "--help" { @@ -44,16 +49,17 @@ func (a *App) Run(args []string) int { return 0 } + // Fallback to new-style interface. + a.cmd.registerNewCommands() + if err := a.cmd.Execute(); err == nil { + return 0 + } + switch err := err.(type) { case *config.ValidationError: printUsage(a.cmd) a.cui.Error(fmt.Sprintf("evans: %s", err.Err)) default: - // Fallback to new-style interface. - a.cmd.registerNewCommands() - if err := a.cmd.Execute(); err == nil { - return 0 - } a.cui.Error(fmt.Sprintf("evans: %s", err)) } return 1 From b4c07f16ffeec29efb4b46cbbd39dd3934e38584 Mon Sep 17 00:00:00 2001 From: ktr0731 Date: Mon, 16 Dec 2019 00:17:02 +0900 Subject: [PATCH 05/12] mark old-style only flags as hidden --- app/commands.go | 9 ++++++++- e2e/cli_test.go | 4 ---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/commands.go b/app/commands.go index 6f40b627..05a01297 100644 --- a/app/commands.go +++ b/app/commands.go @@ -164,12 +164,20 @@ func bindFlags(f *pflag.FlagSet, flags *flags, w io.Writer) { f.BoolVarP(&flags.meta.version, "version", "v", false, "display version and exit") f.BoolVarP(&flags.meta.help, "help", "h", false, "display help text and exit") + // Flags used by old-style only. + for _, name := range []string{"repl", "cli", "call", "file"} { + f.MarkHidden(name) + } + f.Usage = func() { out := w printVersion(out) var buf bytes.Buffer w := tabwriter.NewWriter(&buf, 0, 8, 8, ' ', tabwriter.TabIndent) f.VisitAll(func(f *pflag.Flag) { + if f.Hidden { + return + } cmd := "--" + f.Name if f.Shorthand != "" { cmd += ", -" + f.Shorthand @@ -193,7 +201,6 @@ func newCLICommand(flags *flags, ui cui.UI) *cobra.Command { cmd := &cobra.Command{ Use: "cli", RunE: func(cmd *cobra.Command, args []string) error { - fmt.Println("CLI MODE!") // if err := flags.validate(); err != nil { // return errors.Wrap(err, "invalid flag condition") // } diff --git a/e2e/cli_test.go b/e2e/cli_test.go index 07eab461..237fa31d 100644 --- a/e2e/cli_test.go +++ b/e2e/cli_test.go @@ -357,10 +357,6 @@ Positional arguments: PROTO .proto files Options: - --repl launch Evans as REPL mode (default "false") - --cli start as CLI mode (default "false") - --call string call specified RPC by CLI mode - --file, -f string a script file that will be executed by (used only CLI mode) --silent, -s hide redundant output (default "false") --package string default package --service string default service From a9dac3719e7266fe782042b78e66dde909d29b49 Mon Sep 17 00:00:00 2001 From: ktr0731 Date: Sat, 21 Dec 2019 19:09:38 +0900 Subject: [PATCH 06/12] commonize logic --- app/app.go | 27 +++------ app/commands.go | 149 ++++++++++++++++-------------------------------- 2 files changed, 57 insertions(+), 119 deletions(-) diff --git a/app/app.go b/app/app.go index 0ca63788..0ca26261 100644 --- a/app/app.go +++ b/app/app.go @@ -33,13 +33,15 @@ func New(ui cui.UI) *App { func (a *App) Run(args []string) int { // Currently, Evans is migrating to new-style command-line interface. // So, there are both of old-style and new-style command-line interfaces in this version. - // At first, Evans uses old-style for backward-compatibility, but if it fails, Evans fallbacks to new-style. - // See https://github.com/ktr0731/evans/issues/190 for more details. a.cmd.SetArgs(args) for _, r := range args { - if r == "-h" || r == "--help" { - // Hack. + // Hack. + switch r { + case "cli", "repl": // Sub commands for new-style interface. + // If an arg named "cli" or "repl" is passed, it is regarded as a sub-command of new-style. + a.cmd.registerNewCommands() + case "-h", "--help": // If the help flags is passed, call registerNewCommands for display sub-command helps. a.cmd.registerNewCommands() } @@ -49,19 +51,7 @@ func (a *App) Run(args []string) int { return 0 } - // Fallback to new-style interface. - a.cmd.registerNewCommands() - if err := a.cmd.Execute(); err == nil { - return 0 - } - - switch err := err.(type) { - case *config.ValidationError: - printUsage(a.cmd) - a.cui.Error(fmt.Sprintf("evans: %s", err.Err)) - default: - a.cui.Error(fmt.Sprintf("evans: %s", err)) - } + a.cui.Error(fmt.Sprintf("evans: %s", err)) return 1 } @@ -95,8 +85,7 @@ func mergeConfig(fs *pflag.FlagSet, flags *flags, args []string) (*mergedConfig, if err != nil { return nil, errors.Wrap(err, "failed to get config") } - protos := args - cfg.Default.ProtoFile = append(cfg.Default.ProtoFile, protos...) + cfg.Default.ProtoFile = append(cfg.Default.ProtoFile, args...) if err := cfg.Validate(); err != nil { return nil, err diff --git a/app/commands.go b/app/commands.go index 05a01297..1fb9884d 100644 --- a/app/commands.go +++ b/app/commands.go @@ -5,13 +5,11 @@ import ( "context" "fmt" "io" - "os" "text/tabwriter" "github.com/ktr0731/evans/cache" "github.com/ktr0731/evans/config" "github.com/ktr0731/evans/cui" - "github.com/ktr0731/evans/logger" "github.com/ktr0731/evans/meta" "github.com/ktr0731/evans/mode" "github.com/ktr0731/evans/prompt" @@ -33,17 +31,47 @@ func (c *command) registerNewCommands() { c.AddCommand(newCLICommand(c.flags, c.ui)) } -func newOldCommand(flags *flags, ui cui.UI) *command { - cmd := &cobra.Command{ - RunE: func(cmd *cobra.Command, args []string) error { - if err := flags.validate(); err != nil { - return errors.Wrap(err, "invalid flag condition") - } +// runFunc is a common entrypoint for Run func. +func runFunc( + flags *flags, + f func(*cobra.Command, *mergedConfig) error, +) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + if err := flags.validate(); err != nil { + return errors.Wrap(err, "invalid flag condition") + } + + if flags.meta.help { + printUsage(cmd) + return nil + } - if flags.meta.verbose { - logger.SetOutput(os.Stderr) + // Pass Flags instead of LocalFlags because the config is merged with common and local flags. + cfg, err := mergeConfig(cmd.Flags(), flags, args) + if err != nil { + if err, ok := err.(*config.ValidationError); ok { + printUsage(cmd) + return err } + return errors.Wrap(err, "failed to merge command line flags and config files") + } + + // The entrypoint for the command. + err = f(cmd, cfg) + if err == nil { + return nil + } + switch err.(type) { + case *config.ValidationError: + printUsage(cmd) + } + return err + } +} +func newOldCommand(flags *flags, ui cui.UI) *command { + cmd := &cobra.Command{ + RunE: runFunc(flags, func(cmd *cobra.Command, cfg *mergedConfig) error { switch { case flags.meta.edit: if err := config.Edit(); err != nil { @@ -63,14 +91,6 @@ func newOldCommand(flags *flags, ui cui.UI) *command { return nil } - cfg, err := mergeConfig(cmd.PersistentFlags(), flags, args) - if err != nil { - if err, ok := err.(*config.ValidationError); ok { - return err - } - return errors.Wrap(err, "failed to merge command line flags and config files") - } - if cfg.REPL.ColoredOutput { ui = cui.NewColored(ui) } @@ -113,7 +133,7 @@ func newOldCommand(flags *flags, ui cui.UI) *command { } return nil - }, + }), SilenceErrors: true, SilenceUsage: true, } @@ -166,7 +186,9 @@ func bindFlags(f *pflag.FlagSet, flags *flags, w io.Writer) { // Flags used by old-style only. for _, name := range []string{"repl", "cli", "call", "file"} { - f.MarkHidden(name) + if err := f.MarkHidden(name); err != nil { + panic(fmt.Sprintf("failed to mark %s as hidden: %s", name, err)) + } } f.Usage = func() { @@ -200,91 +222,18 @@ func bindFlags(f *pflag.FlagSet, flags *flags, w io.Writer) { func newCLICommand(flags *flags, ui cui.UI) *cobra.Command { cmd := &cobra.Command{ Use: "cli", - RunE: func(cmd *cobra.Command, args []string) error { - // if err := flags.validate(); err != nil { - // return errors.Wrap(err, "invalid flag condition") - // } - // - // if flags.meta.verbose { - // logger.SetOutput(os.Stderr) - // } - // - // switch { - // case flags.meta.edit: - // if err := config.Edit(); err != nil { - // return errors.Wrap(err, "failed to edit the project local config file") - // } - // return nil - // case flags.meta.editGlobal: - // if err := config.EditGlobal(); err != nil { - // return errors.Wrap(err, "failed to edit the global config file") - // } - // return nil - // case flags.meta.version: - // printVersion(ui.Writer()) - // return nil - // case flags.meta.help: - // printUsage(cmd) - // return nil - // } - // - // cfg, err := mergeConfig(cmd.PersistentFlags(), flags, args) - // if err != nil { - // if err, ok := err.(*config.ValidationError); ok { - // return err - // } - // return errors.Wrap(err, "failed to merge command line flags and config files") - // } - // - // if cfg.REPL.ColoredOutput { - // ui = cui.NewColored(ui) - // } - // - // isCLIMode := (cfg.cli || mode.IsCLIMode(cfg.file)) - // if cfg.repl || !isCLIMode { - // cache, err := cache.Get() - // if err != nil { - // return errors.Wrap(err, "failed to get the cache content") - // } - // - // baseCtx, cancel := context.WithCancel(context.Background()) - // defer cancel() - // eg, ctx := errgroup.WithContext(baseCtx) - // // Run update checker asynchronously. - // eg.Go(func() error { - // return checkUpdate(ctx, cfg.Config, cache) - // }) - // - // if cfg.Config.Meta.AutoUpdate { - // eg.Go(func() error { - // return processUpdate(ctx, cfg.Config, ui.Writer(), cache, prompt.New()) - // }) - // } else if err := processUpdate(ctx, cfg.Config, ui.Writer(), cache, prompt.New()); err != nil { - // return errors.Wrap(err, "failed to update Evans") - // } - // - // if err := mode.RunAsREPLMode(cfg.Config, ui, cache); err != nil { - // return errors.Wrap(err, "failed to run REPL mode") - // } - // - // // Always call cancel func because it is hope to abort update checking if REPL mode is finished - // // before update checking. If update checking is finished before REPL mode, cancel do nothing. - // cancel() - // if err := eg.Wait(); err != nil { - // return errors.Wrap(err, "failed to check application update") - // } - // } else if err := mode.RunAsCLIMode(cfg.Config, cfg.call, cfg.file, ui); err != nil { - // return errors.Wrap(err, "failed to run CLI mode") - // } - + RunE: runFunc(flags, func(_ *cobra.Command, cfg *mergedConfig) error { + if err := mode.RunAsCLIMode(cfg.Config, cfg.call, cfg.file, ui); err != nil { + return errors.Wrap(err, "failed to run CLI mode") + } return nil - }, + }), SilenceErrors: true, SilenceUsage: true, } - bindCLIFlags(cmd.PersistentFlags(), flags, ui.Writer()) + bindCLIFlags(cmd.LocalFlags(), flags, ui.Writer()) cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { - cmd.PersistentFlags().Usage() + cmd.LocalFlags().Usage() }) return cmd } From c985bc219d392d7dd638370f43b5000efb1b5e08 Mon Sep 17 00:00:00 2001 From: ktr0731 Date: Sat, 21 Dec 2019 19:14:11 +0900 Subject: [PATCH 07/12] add repl sub-command --- app/commands.go | 81 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/app/commands.go b/app/commands.go index 1fb9884d..fce602d5 100644 --- a/app/commands.go +++ b/app/commands.go @@ -28,7 +28,10 @@ type command struct { // registerNewCommands registers sub-commands for new-style interface. func (c *command) registerNewCommands() { - c.AddCommand(newCLICommand(c.flags, c.ui)) + c.AddCommand( + newCLICommand(c.flags, c.ui), + newREPLCommand(c.flags, c.ui), + ) } // runFunc is a common entrypoint for Run func. @@ -238,6 +241,53 @@ func newCLICommand(flags *flags, ui cui.UI) *cobra.Command { return cmd } +func newREPLCommand(flags *flags, ui cui.UI) *cobra.Command { + cmd := &cobra.Command{ + Use: "repl", + RunE: runFunc(flags, func(_ *cobra.Command, cfg *mergedConfig) error { + cache, err := cache.Get() + if err != nil { + return errors.Wrap(err, "failed to get the cache content") + } + + baseCtx, cancel := context.WithCancel(context.Background()) + defer cancel() + eg, ctx := errgroup.WithContext(baseCtx) + // Run update checker asynchronously. + eg.Go(func() error { + return checkUpdate(ctx, cfg.Config, cache) + }) + + if cfg.Config.Meta.AutoUpdate { + eg.Go(func() error { + return processUpdate(ctx, cfg.Config, ui.Writer(), cache, prompt.New()) + }) + } else if err := processUpdate(ctx, cfg.Config, ui.Writer(), cache, prompt.New()); err != nil { + return errors.Wrap(err, "failed to update Evans") + } + + if err := mode.RunAsREPLMode(cfg.Config, ui, cache); err != nil { + return errors.Wrap(err, "failed to run REPL mode") + } + + // Always call cancel func because it is hope to abort update checking if REPL mode is finished + // before update checking. If update checking is finished before REPL mode, cancel do nothing. + cancel() + if err := eg.Wait(); err != nil { + return errors.Wrap(err, "failed to check application update") + } + return nil + }), + SilenceErrors: true, + SilenceUsage: true, + } + bindREPLFlags(cmd.LocalFlags(), flags, ui.Writer()) + cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { + cmd.LocalFlags().Usage() + }) + return cmd +} + func bindCLIFlags(f *pflag.FlagSet, flags *flags, w io.Writer) { f.SortFlags = false f.SetOutput(w) @@ -269,3 +319,32 @@ func bindCLIFlags(f *pflag.FlagSet, flags *flags, w io.Writer) { fmt.Fprintf(out, usageFormat, meta.AppName, buf.String()) } } + +func bindREPLFlags(f *pflag.FlagSet, flags *flags, w io.Writer) { + f.SortFlags = false + f.SetOutput(w) + + f.Usage = func() { + out := w + printVersion(out) + var buf bytes.Buffer + w := tabwriter.NewWriter(&buf, 0, 8, 8, ' ', tabwriter.TabIndent) + f.VisitAll(func(f *pflag.Flag) { + cmd := "--" + f.Name + if f.Shorthand != "" { + cmd += ", -" + f.Shorthand + } + name, _ := pflag.UnquoteUsage(f) + if name != "" { + cmd += " " + name + } + usage := f.Usage + if f.DefValue != "" { + usage += fmt.Sprintf(` (default "%s")`, f.DefValue) + } + fmt.Fprintf(w, " %s\t%s\n", cmd, usage) + }) + w.Flush() + fmt.Fprintf(out, usageFormat, meta.AppName, buf.String()) + } +} From 71c3a5ecf9102703e38c6f93385b41765f7c23e2 Mon Sep 17 00:00:00 2001 From: ktr0731 Date: Sun, 22 Dec 2019 16:28:08 +0900 Subject: [PATCH 08/12] set output --- app/commands.go | 46 +++++++++++++++++++++++----------------------- e2e/cli_test.go | 4 ++-- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/app/commands.go b/app/commands.go index fce602d5..59364075 100644 --- a/app/commands.go +++ b/app/commands.go @@ -49,6 +49,25 @@ func runFunc( return nil } + switch { + case flags.meta.edit: + if err := config.Edit(); err != nil { + return errors.Wrap(err, "failed to edit the project local config file") + } + return nil + case flags.meta.editGlobal: + if err := config.EditGlobal(); err != nil { + return errors.Wrap(err, "failed to edit the global config file") + } + return nil + case flags.meta.version: + printVersion(cmd.OutOrStdout()) + return nil + case flags.meta.help: + printUsage(cmd) + return nil + } + // Pass Flags instead of LocalFlags because the config is merged with common and local flags. cfg, err := mergeConfig(cmd.Flags(), flags, args) if err != nil { @@ -64,8 +83,7 @@ func runFunc( if err == nil { return nil } - switch err.(type) { - case *config.ValidationError: + if _, ok := err.(*config.ValidationError); ok { printUsage(cmd) } return err @@ -75,25 +93,6 @@ func runFunc( func newOldCommand(flags *flags, ui cui.UI) *command { cmd := &cobra.Command{ RunE: runFunc(flags, func(cmd *cobra.Command, cfg *mergedConfig) error { - switch { - case flags.meta.edit: - if err := config.Edit(); err != nil { - return errors.Wrap(err, "failed to edit the project local config file") - } - return nil - case flags.meta.editGlobal: - if err := config.EditGlobal(); err != nil { - return errors.Wrap(err, "failed to edit the global config file") - } - return nil - case flags.meta.version: - printVersion(ui.Writer()) - return nil - case flags.meta.help: - printUsage(cmd) - return nil - } - if cfg.REPL.ColoredOutput { ui = cui.NewColored(ui) } @@ -144,6 +143,7 @@ func newOldCommand(flags *flags, ui cui.UI) *command { cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { cmd.PersistentFlags().Usage() }) + cmd.SetOut(ui.Writer()) return &command{cmd, flags, ui} } @@ -281,7 +281,7 @@ func newREPLCommand(flags *flags, ui cui.UI) *cobra.Command { SilenceErrors: true, SilenceUsage: true, } - bindREPLFlags(cmd.LocalFlags(), flags, ui.Writer()) + bindREPLFlags(cmd.LocalFlags(), ui.Writer()) cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { cmd.LocalFlags().Usage() }) @@ -320,7 +320,7 @@ func bindCLIFlags(f *pflag.FlagSet, flags *flags, w io.Writer) { } } -func bindREPLFlags(f *pflag.FlagSet, flags *flags, w io.Writer) { +func bindREPLFlags(f *pflag.FlagSet, w io.Writer) { f.SortFlags = false f.SetOutput(w) diff --git a/e2e/cli_test.go b/e2e/cli_test.go index 237fa31d..7c14fcad 100644 --- a/e2e/cli_test.go +++ b/e2e/cli_test.go @@ -52,12 +52,12 @@ func TestE2E_CLI(t *testing.T) { // unflatten to true. unflatten bool }{ - "print usage text to the ErrWriter": { + "print usage text to the Writer": { args: "--help", expectedOut: expectedUsageOut, unflatten: true, }, - "print version text to the ErrWriter": { + "print version text to the Writer": { args: "--version", expectedOut: fmt.Sprintf("evans %s\n", meta.Version), unflatten: true, From d8f3729efcb07edc96f2358d55ce3aa86a53bca6 Mon Sep 17 00:00:00 2001 From: ktr0731 Date: Sun, 22 Dec 2019 16:39:40 +0900 Subject: [PATCH 09/12] commonize flag initialization --- app/commands.go | 72 ++++++++++--------------------------------------- 1 file changed, 14 insertions(+), 58 deletions(-) diff --git a/app/commands.go b/app/commands.go index 59364075..7761c1f6 100644 --- a/app/commands.go +++ b/app/commands.go @@ -148,8 +148,7 @@ func newOldCommand(flags *flags, ui cui.UI) *command { } func bindFlags(f *pflag.FlagSet, flags *flags, w io.Writer) { - f.SortFlags = false - f.SetOutput(w) + initFlagSet(f, w) f.BoolVar(&flags.mode.repl, "repl", false, "launch Evans as REPL mode") f.BoolVar(&flags.mode.cli, "cli", false, "start as CLI mode") @@ -193,33 +192,6 @@ func bindFlags(f *pflag.FlagSet, flags *flags, w io.Writer) { panic(fmt.Sprintf("failed to mark %s as hidden: %s", name, err)) } } - - f.Usage = func() { - out := w - printVersion(out) - var buf bytes.Buffer - w := tabwriter.NewWriter(&buf, 0, 8, 8, ' ', tabwriter.TabIndent) - f.VisitAll(func(f *pflag.Flag) { - if f.Hidden { - return - } - cmd := "--" + f.Name - if f.Shorthand != "" { - cmd += ", -" + f.Shorthand - } - name, _ := pflag.UnquoteUsage(f) - if name != "" { - cmd += " " + name - } - usage := f.Usage - if f.DefValue != "" { - usage += fmt.Sprintf(` (default "%s")`, f.DefValue) - } - fmt.Fprintf(w, " %s\t%s\n", cmd, usage) - }) - w.Flush() - fmt.Fprintf(out, usageFormat, meta.AppName, buf.String()) - } } func newCLICommand(flags *flags, ui cui.UI) *cobra.Command { @@ -289,47 +261,31 @@ func newREPLCommand(flags *flags, ui cui.UI) *cobra.Command { } func bindCLIFlags(f *pflag.FlagSet, flags *flags, w io.Writer) { - f.SortFlags = false - f.SetOutput(w) - + initFlagSet(f, w) f.StringVar(&flags.cli.call, "call", "", "call specified RPC by CLI mode") f.StringVarP(&flags.cli.file, "file", "f", "", "a script file that will be executed by (used only CLI mode)") - - f.Usage = func() { - out := w - printVersion(out) - var buf bytes.Buffer - w := tabwriter.NewWriter(&buf, 0, 8, 8, ' ', tabwriter.TabIndent) - f.VisitAll(func(f *pflag.Flag) { - cmd := "--" + f.Name - if f.Shorthand != "" { - cmd += ", -" + f.Shorthand - } - name, _ := pflag.UnquoteUsage(f) - if name != "" { - cmd += " " + name - } - usage := f.Usage - if f.DefValue != "" { - usage += fmt.Sprintf(` (default "%s")`, f.DefValue) - } - fmt.Fprintf(w, " %s\t%s\n", cmd, usage) - }) - w.Flush() - fmt.Fprintf(out, usageFormat, meta.AppName, buf.String()) - } } func bindREPLFlags(f *pflag.FlagSet, w io.Writer) { + initFlagSet(f, w) +} + +func initFlagSet(f *pflag.FlagSet, w io.Writer) { f.SortFlags = false f.SetOutput(w) + f.Usage = usageFunc(w, f) +} - f.Usage = func() { - out := w +// usage is the generator for usage output. +func usageFunc(out io.Writer, f *pflag.FlagSet) func() { + return func() { printVersion(out) var buf bytes.Buffer w := tabwriter.NewWriter(&buf, 0, 8, 8, ' ', tabwriter.TabIndent) f.VisitAll(func(f *pflag.Flag) { + if f.Hidden { + return + } cmd := "--" + f.Name if f.Shorthand != "" { cmd += ", -" + f.Shorthand From e05f41456b1243064a9b4b4f05fcc90edcc6be79 Mon Sep 17 00:00:00 2001 From: ktr0731 Date: Sun, 22 Dec 2019 17:10:55 +0900 Subject: [PATCH 10/12] add tests for cli sub-command --- app/commands.go | 6 +- e2e/cli_test.go | 152 ++++++---- e2e/old_cli_test.go | 380 +++++++++++++++++++++++++ e2e/{repl_test.go => old_repl_test.go} | 2 +- 4 files changed, 479 insertions(+), 61 deletions(-) create mode 100644 e2e/old_cli_test.go rename e2e/{repl_test.go => old_repl_test.go} (99%) diff --git a/app/commands.go b/app/commands.go index 7761c1f6..6b4b916f 100644 --- a/app/commands.go +++ b/app/commands.go @@ -253,7 +253,7 @@ func newREPLCommand(flags *flags, ui cui.UI) *cobra.Command { SilenceErrors: true, SilenceUsage: true, } - bindREPLFlags(cmd.LocalFlags(), ui.Writer()) + bindREPLFlags(cmd.LocalFlags(), flags, ui.Writer()) cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { cmd.LocalFlags().Usage() }) @@ -264,10 +264,12 @@ func bindCLIFlags(f *pflag.FlagSet, flags *flags, w io.Writer) { initFlagSet(f, w) f.StringVar(&flags.cli.call, "call", "", "call specified RPC by CLI mode") f.StringVarP(&flags.cli.file, "file", "f", "", "a script file that will be executed by (used only CLI mode)") + f.BoolVarP(&flags.meta.help, "help", "h", false, "display help text and exit") } -func bindREPLFlags(f *pflag.FlagSet, w io.Writer) { +func bindREPLFlags(f *pflag.FlagSet, flags *flags, w io.Writer) { initFlagSet(f, w) + f.BoolVarP(&flags.meta.help, "help", "h", false, "display help text and exit") } func initFlagSet(f *pflag.FlagSet, w io.Writer) { diff --git a/e2e/cli_test.go b/e2e/cli_test.go index 7c14fcad..49ad2d82 100644 --- a/e2e/cli_test.go +++ b/e2e/cli_test.go @@ -19,6 +19,8 @@ func TestE2E_CLI(t *testing.T) { commonFlags := []string{"--verbose"} cases := map[string]struct { + // Common flags all sub-commands can have. + commonFlags string // Space separated arguments text. args string @@ -52,9 +54,19 @@ func TestE2E_CLI(t *testing.T) { // unflatten to true. unflatten bool }{ + "print usage text to the Writer (common flag)": { + commonFlags: "--help", + expectedOut: expectedCLIUsageOut, + unflatten: true, + }, "print usage text to the Writer": { args: "--help", - expectedOut: expectedUsageOut, + expectedOut: expectedCLIUsageOut, + unflatten: true, + }, + "print version text to the Writer (common flag)": { + commonFlags: "--version", + expectedOut: fmt.Sprintf("evans %s\n", meta.Version), unflatten: true, }, "print version text to the Writer": { @@ -78,47 +90,58 @@ func TestE2E_CLI(t *testing.T) { // CLI mode "cannot launch CLI mode because proto files didn't be passed": { - args: "--package api --service Example --call Unary --file testdata/unary_call.in", + commonFlags: "--package api --service Example", + args: "--call Unary --file testdata/unary_call.in", expectedCode: 1, }, "cannot launch CLI mode because --package is invalid value": { - args: "--package foo --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + commonFlags: "--package foo --service Example", + args: "--call Unary --file testdata/unary_call.in testdata/test.proto", expectedCode: 1, }, "cannot launch CLI mode because --service is invalid value": { - args: "--package api --service Foo --call Unary --file testdata/unary_call.in testdata/test.proto", + commonFlags: "--package api --service Foo", + args: "--call Unary --file testdata/unary_call.in testdata/test.proto", expectedCode: 1, }, "cannot launch CLI mode because --call is missing": { - args: "--package api --service Example --file testdata/unary_call.in testdata/test.proto", + commonFlags: "--package api --service Example", + args: "--file testdata/unary_call.in testdata/test.proto", expectedCode: 1, }, "cannot launch CLI mode because --call is invalid value": { - args: "--package api --service Example --call Foo --file testdata/unary_call.in testdata/test.proto", + commonFlags: "--package api --service Example", + args: "--call Foo --file testdata/unary_call.in testdata/test.proto", expectedCode: 1, }, "cannot launch CLI mode because the path of --file is invalid path": { - args: "--package api --service Example --call Unary --file foo testdata/test.proto", + commonFlags: "--package api --service Example", + args: "--call Unary --file foo testdata/test.proto", expectedCode: 1, }, "cannot launch CLI mode because the path of --file is invalid input": { - args: "--package api --service Example --call Unary --file testdata/invalid.in testdata/test.proto", + commonFlags: "--package api --service Example", + args: "--call Unary --file testdata/invalid.in testdata/test.proto", expectedCode: 1, }, "cannot launch CLI mode because --header didn't have value": { - args: "--header foo --package api --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + commonFlags: "--header foo --package api --service Example", + args: "--call Unary --file testdata/unary_call.in testdata/test.proto", expectedCode: 1, }, "cannot launch CLI mode because --header has an invalid form": { - args: "--header foo=bar=baz --package api --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + commonFlags: "--header foo=bar=baz --package api --service Example", + args: "--call Unary --file testdata/unary_call.in testdata/test.proto", expectedCode: 1, }, "call unary RPC with an input file by CLI mode": { - args: "--package api --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + commonFlags: "--package api --service Example", + args: "--call Unary --file testdata/unary_call.in testdata/test.proto", expectedOut: `{ "message": "hello, oumae" }`, }, "call unary RPC without package name because the size of packages is 1": { - args: "--service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + commonFlags: "--service Example --call Unary", + args: "--file testdata/unary_call.in testdata/test.proto", expectedOut: `{ "message": "hello, oumae" }`, }, "call unary RPC without package and service name because the size of packages and services are 1": { @@ -126,7 +149,8 @@ func TestE2E_CLI(t *testing.T) { expectedOut: `{ "message": "hello, oumae" }`, }, "call unary RPC with an input reader by CLI mode": { - args: "--package api --service Example --call Unary testdata/test.proto", + commonFlags: "--package api --service Example", + args: "--call Unary testdata/test.proto", beforeTest: func(t *testing.T) func(*testing.T) { old := mode.DefaultCLIReader mode.DefaultCLIReader = strings.NewReader(`{"name": "oumae"}`) @@ -137,15 +161,18 @@ func TestE2E_CLI(t *testing.T) { expectedOut: `{ "message": "hello, oumae" }`, }, "call client streaming RPC by CLI mode": { - args: "--package api --service Example --call ClientStreaming --file testdata/client_streaming.in testdata/test.proto", + commonFlags: "--package api --service Example", + args: "--call ClientStreaming --file testdata/client_streaming.in testdata/test.proto", expectedOut: `{ "message": "you sent requests 4 times (oumae, kousaka, kawashima, kato)." }`, }, "call server streaming RPC by CLI mode": { - args: "--package api --service Example --call ServerStreaming --file testdata/server_streaming.in testdata/test.proto", + commonFlags: "--package api --service Example", + args: "--call ServerStreaming --file testdata/server_streaming.in testdata/test.proto", expectedOut: `{ "message": "hello oumae, I greet 1 times." } { "message": "hello oumae, I greet 2 times." } { "message": "hello oumae, I greet 3 times." }`, }, "call bidi streaming RPC by CLI mode": { - args: "--package api --service Example --call BidiStreaming --file testdata/bidi_streaming.in testdata/test.proto", + commonFlags: "--package api --service Example", + args: "--call BidiStreaming --file testdata/bidi_streaming.in testdata/test.proto", assertTest: func(t *testing.T, output string) { dec := json.NewDecoder(strings.NewReader(output)) for { @@ -161,7 +188,8 @@ func TestE2E_CLI(t *testing.T) { }, }, "call unary RPC with an input file and custom headers by CLI mode": { - args: "--header ogiso=setsuna --header touma=kazusa,youko --package api --service Example --call UnaryHeader --file testdata/unary_header.in testdata/test.proto", + commonFlags: "--header ogiso=setsuna --header touma=kazusa,youko --package api --service Example", + args: "--call UnaryHeader --file testdata/unary_header.in testdata/test.proto", assertTest: func(t *testing.T, output string) { expectedStrings := []string{ "key = ogiso", @@ -180,17 +208,20 @@ func TestE2E_CLI(t *testing.T) { // CLI mode with reflection "cannot launch CLI mode with reflection because --call is missing": { - args: "--reflection --package api --service Example testdata/test.proto --file testdata/unary_call.in", + commonFlags: "--reflection --package api --service Example", + args: "testdata/test.proto --file testdata/unary_call.in", reflection: true, expectedCode: 1, }, "cannot launch CLI mode with reflection because server didn't enable reflection": { - args: "--reflection --package api --service Example --call Unary --file testdata/unary_call.in", + commonFlags: "--reflection --package api --service Example", + args: "--call Unary --file testdata/unary_call.in", reflection: false, expectedCode: 1, }, "call unary RPC by CLI mode with reflection with an input file": { - args: "--reflection --package api --service Example --call Unary --file testdata/unary_call.in", + commonFlags: "--reflection --package api --service Example", + args: "--call Unary --file testdata/unary_call.in", reflection: true, expectedOut: `{ "message": "hello, oumae" }`, }, @@ -198,59 +229,70 @@ func TestE2E_CLI(t *testing.T) { // CLI mode with TLS "cannot launch CLI mode with TLS because the server didn't enable TLS": { - args: "--tls --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + commonFlags: "--tls --service Example", + args: "--call Unary --file testdata/unary_call.in testdata/test.proto", tls: false, expectedCode: 1, }, "cannot launch CLI mode with TLS because the client didn't enable TLS": { - args: "--service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + commonFlags: "--service Example", + args: "--call Unary --file testdata/unary_call.in testdata/test.proto", tls: true, expectedCode: 1, }, "cannot launch CLI mode with TLS because cannot validate certs for 127.0.0.1 (default value)": { - args: "--tls --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + commonFlags: "--tls --service Example", + args: "--call Unary --file testdata/unary_call.in testdata/test.proto", tls: true, expectedCode: 1, }, "cannot launch CLI mode with TLS because signed authority is unknown": { - args: "--tls --host localhost --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + commonFlags: "--tls --host localhost --service Example", + args: "--call Unary --file testdata/unary_call.in testdata/test.proto", tls: true, expectedCode: 1, }, "call unary RPC with TLS by CLI mode": { - args: "--tls --host localhost --cacert testdata/rootCA.pem --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + commonFlags: "--tls --host localhost --cacert testdata/rootCA.pem --service Example", + args: "--call Unary --file testdata/unary_call.in testdata/test.proto", tls: true, expectedOut: `{ "message": "hello, oumae" }`, }, "cannot launch CLI mode with TLS and reflection by CLI mode because server didn't enable TLS": { - args: "--tls -r --host localhost --cacert testdata/rootCA.pem --service Example --call Unary --file testdata/unary_call.in", + commonFlags: "--tls -r --host localhost --cacert testdata/rootCA.pem --service Example", + args: "--call Unary --file testdata/unary_call.in", tls: false, reflection: true, expectedCode: 1, }, "call unary RPC with TLS and reflection by CLI mode": { - args: "--tls -r --host localhost --cacert testdata/rootCA.pem --service Example --call Unary --file testdata/unary_call.in", + commonFlags: "--tls -r --host localhost --cacert testdata/rootCA.pem --service Example", + args: "--call Unary --file testdata/unary_call.in", tls: true, reflection: true, expectedOut: `{ "message": "hello, oumae" }`, }, "call unary RPC with TLS and --servername by CLI mode": { - args: "--tls --servername localhost --cacert testdata/rootCA.pem --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + commonFlags: "--tls --servername localhost --cacert testdata/rootCA.pem --service Example", + args: "--call Unary --file testdata/unary_call.in testdata/test.proto", tls: true, expectedOut: `{ "message": "hello, oumae" }`, }, "cannot launch CLI mode with mutual TLS auth because --certkey is missing": { - args: "--tls --host localhost --cacert testdata/rootCA.pem --cert testdata/localhost.pem --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + commonFlags: "--tls --host localhost --cacert testdata/rootCA.pem --cert testdata/localhost.pem --service Example", + args: "--call Unary --file testdata/unary_call.in testdata/test.proto", tls: true, expectedCode: 1, }, "cannot launch CLI mode with mutual TLS auth because --cert is missing": { - args: "--tls --host localhost --cacert testdata/rootCA.pem --certkey testdata/localhost-key.pem --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + commonFlags: "--tls --host localhost --cacert testdata/rootCA.pem --certkey testdata/localhost-key.pem --service Example", + args: "--call Unary --file testdata/unary_call.in testdata/test.proto", tls: true, expectedCode: 1, }, "call unary RPC with mutual TLS auth by CLI mode": { - args: "--tls --host localhost --cacert testdata/rootCA.pem --cert testdata/localhost.pem --certkey testdata/localhost-key.pem --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + commonFlags: "--tls --host localhost --cacert testdata/rootCA.pem --cert testdata/localhost.pem --certkey testdata/localhost-key.pem --service Example", + args: "--call Unary --file testdata/unary_call.in testdata/test.proto", tls: true, expectedOut: `{ "message": "hello, oumae" }`, }, @@ -258,34 +300,40 @@ func TestE2E_CLI(t *testing.T) { // CLI mode with gRPC-Web "cannot send a request to gRPC-Web server because the server didn't enable gRPC-Web": { - args: "--web --package api --service Example --call Unary --file testdata/unary_call.in testdata/test.proto testdata/test.proto", + commonFlags: "--web --package api --service Example", + args: "--call Unary --file testdata/unary_call.in testdata/test.proto testdata/test.proto", web: false, expectedCode: 1, }, "call unary RPC with an input file by CLI mode against to gRPC-Web server": { - args: "--web --package api --service Example --call Unary --file testdata/unary_call.in testdata/test.proto testdata/test.proto", + commonFlags: "--web --package api --service Example", + args: "--call Unary --file testdata/unary_call.in testdata/test.proto testdata/test.proto", web: true, expectedOut: `{ "message": "hello, oumae" }`, }, "call unary RPC with an input file by CLI mode and reflection against to gRPC-Web server": { - args: "--web -r --service Example --call Unary --file testdata/unary_call.in", + commonFlags: "--web -r --service Example", + args: "--call Unary --file testdata/unary_call.in", web: true, reflection: true, expectedOut: `{ "message": "hello, oumae" }`, }, "call client streaming RPC by CLI mode against to gRPC-Web server": { - args: "--web --service Example --call ClientStreaming --file testdata/client_streaming.in testdata/test.proto", + commonFlags: "--web --service Example", + args: "--call ClientStreaming --file testdata/client_streaming.in testdata/test.proto", web: true, expectedOut: `{ "message": "you sent requests 4 times (oumae, kousaka, kawashima, kato)." }`, }, "call server streaming RPC by CLI mode against to gRPC-Web server": { - args: "--web --service Example --call ServerStreaming --file testdata/server_streaming.in testdata/test.proto", + commonFlags: "--web --service Example", + args: "--call ServerStreaming --file testdata/server_streaming.in testdata/test.proto", web: true, expectedOut: `{ "message": "hello oumae, I greet 1 times." } { "message": "hello oumae, I greet 2 times." } { "message": "hello oumae, I greet 3 times." }`, }, "call bidi streaming RPC by CLI mode against to gRPC-Web server": { - args: "--web --service Example --call BidiStreaming --file testdata/bidi_streaming.in testdata/test.proto", - web: true, + commonFlags: "--web --service Example", + args: "--call BidiStreaming --file testdata/bidi_streaming.in testdata/test.proto", + web: true, assertTest: func(t *testing.T, output string) { dec := json.NewDecoder(strings.NewReader(output)) for { @@ -312,6 +360,10 @@ func TestE2E_CLI(t *testing.T) { args := commonFlags args = append([]string{"--port", port}, args...) + if c.commonFlags != "" { + args = append(args, strings.Split(c.commonFlags, " ")...) + } + args = append(args, "cli") // Sub-command name. if c.args != "" { args = append(args, strings.Split(c.args, " ")...) } @@ -349,7 +401,7 @@ func TestE2E_CLI(t *testing.T) { } } -var expectedUsageOut = fmt.Sprintf(`evans %s +var expectedCLIUsageOut = fmt.Sprintf(`evans %s Usage: evans [--help] [--version] [options ...] [PROTO [PROTO ...]] @@ -357,24 +409,8 @@ Positional arguments: PROTO .proto files Options: - --silent, -s hide redundant output (default "false") - --package string default package - --service string default service - --path strings proto file paths (default "[]") - --host string gRPC server host - --port, -p string gRPC server port (default "50051") - --header slice of strings default headers that set to each requests (example: foo=bar) (default "[]") - --web use gRPC-Web protocol (default "false") - --reflection, -r use gRPC reflection (default "false") - --tls, -t use a secure TLS connection (default "false") - --cacert string the CA certificate file for verifying the server - --cert string the certificate file for mutual TLS auth. it must be provided with --certkey. - --certkey string the private key file for mutual TLS auth. it must be provided with --cert. - --servername string override the server name used to verify the hostname (ignored if --tls is disabled) - --edit, -e edit the project config file by using $EDITOR (default "false") - --edit-global edit the global config file by using $EDITOR (default "false") - --verbose verbose output (default "false") - --version, -v display version and exit (default "false") - --help, -h display help text and exit (default "false") + --call string call specified RPC by CLI mode + --file, -f string a script file that will be executed by (used only CLI mode) + --help, -h display help text and exit (default "false") `, meta.Version) diff --git a/e2e/old_cli_test.go b/e2e/old_cli_test.go new file mode 100644 index 00000000..26b612a6 --- /dev/null +++ b/e2e/old_cli_test.go @@ -0,0 +1,380 @@ +package e2e_test + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/ktr0731/evans/app" + "github.com/ktr0731/evans/cui" + "github.com/ktr0731/evans/meta" + "github.com/ktr0731/evans/mode" +) + +func TestE2E_OldCLI(t *testing.T) { + commonFlags := []string{"--verbose"} + + cases := map[string]struct { + // Space separated arguments text. + args string + + // The server enables TLS. + tls bool + + // The server enables gRPC reflection. + reflection bool + + // The server uses gRPC-Web protocol. + web bool + + // beforeTest set up a testcase specific environment. + // If beforeTest is nil, it is ignored. + // beforeTest may return a function named afterTest that cleans up + // the environment. If afterTest is nil, it is ignored. + beforeTest func(t *testing.T) (afterTest func(t *testing.T)) + + // assertTest checks whether the output is expected. + // If nil, it will be ignored. + assertTest func(t *testing.T, output string) + + // The output we expected. It is ignored if expectedCode isn't 0. + expectedOut string + + // The exit code we expected. + expectedCode int + + // Each output is formatted by flatten() for remove break lines. + // But, if it is prefer to show as it is, you can it by specifying + // unflatten to true. + unflatten bool + }{ + "print usage text to the Writer": { + args: "--help", + expectedOut: expectedUsageOut, + unflatten: true, + }, + "print version text to the Writer": { + args: "--version", + expectedOut: fmt.Sprintf("evans %s\n", meta.Version), + unflatten: true, + }, + "cannot specify both of --cli and --repl": { + args: "--cli --repl", + expectedCode: 1, + }, + "cannot specify both of --tls and --web": { + args: "--web --tls testdata/test.proto", + expectedCode: 1, + }, + "cannot launch without proto files and reflection": { + args: "", + expectedCode: 1, + }, + + // CLI mode + + "cannot launch CLI mode because proto files didn't be passed": { + args: "--package api --service Example --call Unary --file testdata/unary_call.in", + expectedCode: 1, + }, + "cannot launch CLI mode because --package is invalid value": { + args: "--package foo --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + expectedCode: 1, + }, + "cannot launch CLI mode because --service is invalid value": { + args: "--package api --service Foo --call Unary --file testdata/unary_call.in testdata/test.proto", + expectedCode: 1, + }, + "cannot launch CLI mode because --call is missing": { + args: "--package api --service Example --file testdata/unary_call.in testdata/test.proto", + expectedCode: 1, + }, + "cannot launch CLI mode because --call is invalid value": { + args: "--package api --service Example --call Foo --file testdata/unary_call.in testdata/test.proto", + expectedCode: 1, + }, + "cannot launch CLI mode because the path of --file is invalid path": { + args: "--package api --service Example --call Unary --file foo testdata/test.proto", + expectedCode: 1, + }, + "cannot launch CLI mode because the path of --file is invalid input": { + args: "--package api --service Example --call Unary --file testdata/invalid.in testdata/test.proto", + expectedCode: 1, + }, + "cannot launch CLI mode because --header didn't have value": { + args: "--header foo --package api --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + expectedCode: 1, + }, + "cannot launch CLI mode because --header has an invalid form": { + args: "--header foo=bar=baz --package api --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + expectedCode: 1, + }, + "call unary RPC with an input file by CLI mode": { + args: "--package api --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + expectedOut: `{ "message": "hello, oumae" }`, + }, + "call unary RPC without package name because the size of packages is 1": { + args: "--service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + expectedOut: `{ "message": "hello, oumae" }`, + }, + "call unary RPC without package and service name because the size of packages and services are 1": { + args: "--call Unary --file testdata/unary_call.in testdata/test.proto", + expectedOut: `{ "message": "hello, oumae" }`, + }, + "call unary RPC with an input reader by CLI mode": { + args: "--package api --service Example --call Unary testdata/test.proto", + beforeTest: func(t *testing.T) func(*testing.T) { + old := mode.DefaultCLIReader + mode.DefaultCLIReader = strings.NewReader(`{"name": "oumae"}`) + return func(t *testing.T) { + mode.DefaultCLIReader = old + } + }, + expectedOut: `{ "message": "hello, oumae" }`, + }, + "call client streaming RPC by CLI mode": { + args: "--package api --service Example --call ClientStreaming --file testdata/client_streaming.in testdata/test.proto", + expectedOut: `{ "message": "you sent requests 4 times (oumae, kousaka, kawashima, kato)." }`, + }, + "call server streaming RPC by CLI mode": { + args: "--package api --service Example --call ServerStreaming --file testdata/server_streaming.in testdata/test.proto", + expectedOut: `{ "message": "hello oumae, I greet 1 times." } { "message": "hello oumae, I greet 2 times." } { "message": "hello oumae, I greet 3 times." }`, + }, + "call bidi streaming RPC by CLI mode": { + args: "--package api --service Example --call BidiStreaming --file testdata/bidi_streaming.in testdata/test.proto", + assertTest: func(t *testing.T, output string) { + dec := json.NewDecoder(strings.NewReader(output)) + for { + var iface interface{} + err := dec.Decode(&iface) + if err == io.EOF { + return + } + if err != nil { + t.Errorf("expected no errors, but got '%s'", err) + } + } + }, + }, + "call unary RPC with an input file and custom headers by CLI mode": { + args: "--header ogiso=setsuna --header touma=kazusa,youko --package api --service Example --call UnaryHeader --file testdata/unary_header.in testdata/test.proto", + assertTest: func(t *testing.T, output string) { + expectedStrings := []string{ + "key = ogiso", + "val = setsuna", + "key = touma", + "val = kazusa, youko", + } + for _, s := range expectedStrings { + if !strings.Contains(output, s) { + t.Errorf("expected to contain '%s', but missing in '%s'", s, output) + } + } + }, + }, + + // CLI mode with reflection + + "cannot launch CLI mode with reflection because --call is missing": { + args: "--reflection --package api --service Example testdata/test.proto --file testdata/unary_call.in", + reflection: true, + expectedCode: 1, + }, + "cannot launch CLI mode with reflection because server didn't enable reflection": { + args: "--reflection --package api --service Example --call Unary --file testdata/unary_call.in", + reflection: false, + expectedCode: 1, + }, + "call unary RPC by CLI mode with reflection with an input file": { + args: "--reflection --package api --service Example --call Unary --file testdata/unary_call.in", + reflection: true, + expectedOut: `{ "message": "hello, oumae" }`, + }, + + // CLI mode with TLS + + "cannot launch CLI mode with TLS because the server didn't enable TLS": { + args: "--tls --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + tls: false, + expectedCode: 1, + }, + "cannot launch CLI mode with TLS because the client didn't enable TLS": { + args: "--service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + tls: true, + expectedCode: 1, + }, + "cannot launch CLI mode with TLS because cannot validate certs for 127.0.0.1 (default value)": { + args: "--tls --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + tls: true, + expectedCode: 1, + }, + "cannot launch CLI mode with TLS because signed authority is unknown": { + args: "--tls --host localhost --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + tls: true, + expectedCode: 1, + }, + "call unary RPC with TLS by CLI mode": { + args: "--tls --host localhost --cacert testdata/rootCA.pem --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + tls: true, + expectedOut: `{ "message": "hello, oumae" }`, + }, + "cannot launch CLI mode with TLS and reflection by CLI mode because server didn't enable TLS": { + args: "--tls -r --host localhost --cacert testdata/rootCA.pem --service Example --call Unary --file testdata/unary_call.in", + tls: false, + reflection: true, + expectedCode: 1, + }, + "call unary RPC with TLS and reflection by CLI mode": { + args: "--tls -r --host localhost --cacert testdata/rootCA.pem --service Example --call Unary --file testdata/unary_call.in", + tls: true, + reflection: true, + expectedOut: `{ "message": "hello, oumae" }`, + }, + "call unary RPC with TLS and --servername by CLI mode": { + args: "--tls --servername localhost --cacert testdata/rootCA.pem --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + tls: true, + expectedOut: `{ "message": "hello, oumae" }`, + }, + "cannot launch CLI mode with mutual TLS auth because --certkey is missing": { + args: "--tls --host localhost --cacert testdata/rootCA.pem --cert testdata/localhost.pem --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + tls: true, + expectedCode: 1, + }, + "cannot launch CLI mode with mutual TLS auth because --cert is missing": { + args: "--tls --host localhost --cacert testdata/rootCA.pem --certkey testdata/localhost-key.pem --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + tls: true, + expectedCode: 1, + }, + "call unary RPC with mutual TLS auth by CLI mode": { + args: "--tls --host localhost --cacert testdata/rootCA.pem --cert testdata/localhost.pem --certkey testdata/localhost-key.pem --service Example --call Unary --file testdata/unary_call.in testdata/test.proto", + tls: true, + expectedOut: `{ "message": "hello, oumae" }`, + }, + + // CLI mode with gRPC-Web + + "cannot send a request to gRPC-Web server because the server didn't enable gRPC-Web": { + args: "--web --package api --service Example --call Unary --file testdata/unary_call.in testdata/test.proto testdata/test.proto", + web: false, + expectedCode: 1, + }, + "call unary RPC with an input file by CLI mode against to gRPC-Web server": { + args: "--web --package api --service Example --call Unary --file testdata/unary_call.in testdata/test.proto testdata/test.proto", + web: true, + expectedOut: `{ "message": "hello, oumae" }`, + }, + "call unary RPC with an input file by CLI mode and reflection against to gRPC-Web server": { + args: "--web -r --service Example --call Unary --file testdata/unary_call.in", + web: true, + reflection: true, + expectedOut: `{ "message": "hello, oumae" }`, + }, + "call client streaming RPC by CLI mode against to gRPC-Web server": { + args: "--web --service Example --call ClientStreaming --file testdata/client_streaming.in testdata/test.proto", + web: true, + expectedOut: `{ "message": "you sent requests 4 times (oumae, kousaka, kawashima, kato)." }`, + }, + "call server streaming RPC by CLI mode against to gRPC-Web server": { + args: "--web --service Example --call ServerStreaming --file testdata/server_streaming.in testdata/test.proto", + web: true, + expectedOut: `{ "message": "hello oumae, I greet 1 times." } { "message": "hello oumae, I greet 2 times." } { "message": "hello oumae, I greet 3 times." }`, + }, + "call bidi streaming RPC by CLI mode against to gRPC-Web server": { + args: "--web --service Example --call BidiStreaming --file testdata/bidi_streaming.in testdata/test.proto", + web: true, + assertTest: func(t *testing.T, output string) { + dec := json.NewDecoder(strings.NewReader(output)) + for { + var iface interface{} + err := dec.Decode(&iface) + if err == io.EOF { + return + } + if err != nil { + t.Errorf("expected no errors, but got '%s'", err) + } + } + }, + }, + } + for name, c := range cases { + c := c + t.Run(name, func(t *testing.T) { + stopServer, port := startServer(t, c.tls, c.reflection, c.web) + defer stopServer() + + outBuf, eoutBuf := new(bytes.Buffer), new(bytes.Buffer) + cui := cui.New(cui.Writer(outBuf), cui.ErrWriter(eoutBuf)) + + args := commonFlags + args = append([]string{"--port", port}, args...) + if c.args != "" { + args = append(args, strings.Split(c.args, " ")...) + } + + if c.beforeTest != nil { + afterTest := c.beforeTest(t) + if afterTest != nil { + defer afterTest(t) + } + } + + a := app.New(cui) + code := a.Run(args) + if code != c.expectedCode { + t.Errorf("unexpected code returned: expected = %d, actual = %d", c.expectedCode, code) + } + + actual := outBuf.String() + if !c.unflatten { + actual = flatten(actual) + } + + if c.expectedCode == 0 { + if c.assertTest != nil { + c.assertTest(t, actual) + } + if c.expectedOut != "" && actual != c.expectedOut { + t.Errorf("unexpected output:\n%s", cmp.Diff(c.expectedOut, actual)) + } + if eoutBuf.String() != "" { + t.Errorf("expected code is 0, but got an error message: '%s'", eoutBuf.String()) + } + } + }) + } +} + +var expectedUsageOut = fmt.Sprintf(`evans %s + +Usage: evans [--help] [--version] [options ...] [PROTO [PROTO ...]] + +Positional arguments: + PROTO .proto files + +Options: + --silent, -s hide redundant output (default "false") + --package string default package + --service string default service + --path strings proto file paths (default "[]") + --host string gRPC server host + --port, -p string gRPC server port (default "50051") + --header slice of strings default headers that set to each requests (example: foo=bar) (default "[]") + --web use gRPC-Web protocol (default "false") + --reflection, -r use gRPC reflection (default "false") + --tls, -t use a secure TLS connection (default "false") + --cacert string the CA certificate file for verifying the server + --cert string the certificate file for mutual TLS auth. it must be provided with --certkey. + --certkey string the private key file for mutual TLS auth. it must be provided with --cert. + --servername string override the server name used to verify the hostname (ignored if --tls is disabled) + --edit, -e edit the project config file by using $EDITOR (default "false") + --edit-global edit the global config file by using $EDITOR (default "false") + --verbose verbose output (default "false") + --version, -v display version and exit (default "false") + --help, -h display help text and exit (default "false") + +`, meta.Version) diff --git a/e2e/repl_test.go b/e2e/old_repl_test.go similarity index 99% rename from e2e/repl_test.go rename to e2e/old_repl_test.go index 86ffa411..c3f22c50 100644 --- a/e2e/repl_test.go +++ b/e2e/old_repl_test.go @@ -32,7 +32,7 @@ func init() { } } -func TestE2E_REPL(t *testing.T) { +func TestE2E_OldREPL(t *testing.T) { // In testing, Go modifies stdin, so we specify --repl explicitly. commonFlags := []string{"--silent", "--repl"} From f407b24a58a78e8e0f0e5dbd646d2fb974e80a42 Mon Sep 17 00:00:00 2001 From: ktr0731 Date: Sun, 22 Dec 2019 17:29:47 +0900 Subject: [PATCH 11/12] update golden files --- app/commands.go | 10 +- e2e/repl_test.go | 306 ++++++++++++++++++ .../teste2e_oldrepl-add_a_header.golden | 8 + .../teste2e_oldrepl-add_two_headers.golden | 9 + ...drepl-add_two_values_in_one_command.golden | 9 + ...e2e_oldrepl-add_two_values_to_a_key.golden | 10 + ...ming_rpc_against_to_grpc-web_server.golden | 37 +++ .../teste2e_oldrepl-call_bidistreaming.golden | 28 ++ ...ming_rpc_against_to_grpc-web_server.golden | 4 + ...este2e_oldrepl-call_clientstreaming.golden | 4 + ...ming_rpc_against_to_grpc-web_server.golden | 10 + ...all_unary_by_selecting_only_service.golden | 5 + ...ry_by_selecting_package_and_service.golden | 6 + ...-call_unary_by_specifying_--service.golden | 4 + .../teste2e_oldrepl-call_unarybytes.golden | 4 + .../teste2e_oldrepl-call_unaryenum.golden | 4 + .../teste2e_oldrepl-call_unarymessage.golden | 4 + .../teste2e_oldrepl-call_unaryoneof.golden | 4 + .../teste2e_oldrepl-call_unaryrepeated.golden | 4 + ...te2e_oldrepl-call_unaryrepeatedenum.golden | 4 + .../teste2e_oldrepl-call_unaryself.golden | 4 + ...c_is_also_enabled_in_streaming_rpcs.golden | 28 ++ ...if_there_are_no_message_type_fields.golden | 4 + ...ips_the_rest_of_the_current_message.golden | 4 + ...essage_and_exits_the_repeated_field.golden | 4 + .../teste2e_oldrepl-desc_a_map.golden | 7 + ...e2e_oldrepl-desc_a_repeated_message.golden | 6 + ...teste2e_oldrepl-desc_simple_message.golden | 6 + .../teste2e_oldrepl-remove_a_header.golden | 6 + .../teste2e_oldrepl-show_--help.golden | 2 + .../teste2e_oldrepl-show_message.golden | 18 ++ .../teste2e_oldrepl-show_package.golden | 6 + .../fixtures/teste2e_oldrepl-show_rpc.golden | 20 ++ .../teste2e_oldrepl-show_service.golden | 20 ++ 34 files changed, 601 insertions(+), 8 deletions(-) create mode 100644 e2e/repl_test.go create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-add_a_header.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-add_two_headers.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-add_two_values_in_one_command.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-add_two_values_to_a_key.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-call_bidi_streaming_rpc_against_to_grpc-web_server.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-call_bidistreaming.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-call_client_streaming_rpc_against_to_grpc-web_server.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-call_clientstreaming.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-call_server_streaming_rpc_against_to_grpc-web_server.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-call_unary_by_selecting_only_service.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-call_unary_by_selecting_package_and_service.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-call_unary_by_specifying_--service.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-call_unarybytes.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-call_unaryenum.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-call_unarymessage.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-call_unaryoneof.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-call_unaryrepeated.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-call_unaryrepeatedenum.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-call_unaryself.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-ctrl-c_is_also_enabled_in_streaming_rpcs.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-ctrl-c_skips_the_rest_of_fields_if_there_are_no_message_type_fields.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-ctrl-c_skips_the_rest_of_the_current_message.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-ctrl-c_skips_the_rest_of_the_current_message_and_exits_the_repeated_field.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-desc_a_map.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-desc_a_repeated_message.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-desc_simple_message.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-remove_a_header.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-show_--help.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-show_message.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-show_package.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-show_rpc.golden create mode 100644 e2e/testdata/fixtures/teste2e_oldrepl-show_service.golden diff --git a/app/commands.go b/app/commands.go index 6b4b916f..7df23443 100644 --- a/app/commands.go +++ b/app/commands.go @@ -44,11 +44,6 @@ func runFunc( return errors.Wrap(err, "invalid flag condition") } - if flags.meta.help { - printUsage(cmd) - return nil - } - switch { case flags.meta.edit: if err := config.Edit(); err != nil { @@ -63,9 +58,8 @@ func runFunc( case flags.meta.version: printVersion(cmd.OutOrStdout()) return nil - case flags.meta.help: - printUsage(cmd) - return nil + + // Help is processed by cobra. } // Pass Flags instead of LocalFlags because the config is merged with common and local flags. diff --git a/e2e/repl_test.go b/e2e/repl_test.go new file mode 100644 index 00000000..2414db68 --- /dev/null +++ b/e2e/repl_test.go @@ -0,0 +1,306 @@ +package e2e_test + +import ( + "bytes" + "io" + "strings" + "testing" + + "github.com/ktr0731/evans/app" + "github.com/ktr0731/evans/cui" + "github.com/ktr0731/evans/prompt" +) + +func TestE2E_REPL(t *testing.T) { + commonFlags := []string{"--silent"} + + cases := map[string]struct { + input []interface{} + + // Common flags all sub-commands can have. + commonFlags string + // Space separated arguments text. + args string + + // The server enables TLS. + tls bool + + // The server enables gRPC reflection. + reflection bool + + // The server uses gRPC-Web protocol. + web bool + + // The exit code we expected. + expectedCode int + + // skipGolden skips golden file testing. + skipGolden bool + + // hasErr checks whether REPL wrote some errors to UI.ErrWriter. + hasErr bool + }{ + // RPC calls. + + "call Unary by selecting package and service": { + args: "testdata/test.proto", + input: []interface{}{"package api", "service Example", "call Unary", "kaguya"}, + }, + "call Unary by selecting only service": { + args: "testdata/test.proto", + input: []interface{}{"service Example", "call Unary", "kaguya"}, + }, + "call Unary by specifying --service": { + commonFlags: "--service Example", + args: "testdata/test.proto", + input: []interface{}{"call Unary", "kaguya"}, + }, + "call ClientStreaming": { + args: "testdata/test.proto", + // io.EOF means end of inputting. + input: []interface{}{"call ClientStreaming", "kaguya", "chika", "miko", io.EOF}, + }, + "call BidiStreaming": { + args: "testdata/test.proto", + // io.EOF means end of inputting. + input: []interface{}{"call BidiStreaming", "kaguya", "chika", "miko", io.EOF}, + }, + "call UnaryMessage": { + args: "testdata/test.proto", + input: []interface{}{"call UnaryMessage", "kaguya", "shinomiya"}, + }, + "call UnaryRepeated": { + args: "testdata/test.proto", + input: []interface{}{"call UnaryRepeated", "miyuki", "kaguya", "chika", "yu", io.EOF}, + }, + "call UnarySelf": { + args: "testdata/test.proto", + input: []interface{}{"call UnarySelf", "dig down", "ohana", "matsumae", "ohana", "dig down", "nako", "oshimizu", "nakochi", "finish", "dig down", "minko", "tsurugi", "minchi", "finish", "finish"}, + }, + "call UnaryMap": { + args: "testdata/test.proto", + input: []interface{}{"call UnaryMap", "key1", "val1", "key2", "val2", io.EOF}, + skipGolden: true, + }, + "call UnaryOneof": { + args: "testdata/test.proto", + input: []interface{}{"call UnaryOneof", "msg", "ai", "hayasaka"}, + }, + "call UnaryEnum": { + args: "testdata/test.proto", + input: []interface{}{"call UnaryEnum", "Male"}, + }, + "call UnaryBytes": { + args: "testdata/test.proto", + input: []interface{}{"call UnaryBytes", "\\u3084\\u306f\\u308a\\u4ffa\\u306e\\u9752\\u6625\\u30e9\\u30d6\\u30b3\\u30e1\\u306f\\u307e\\u3061\\u304c\\u3063\\u3066\\u3044\\u308b\\u3002"}, + }, + "call UnaryRepeatedEnum": { + args: "testdata/test.proto", + input: []interface{}{"call UnaryRepeatedEnum", "Male", "Male", "Female", io.EOF}, + }, + + // call (gRPC-Web) + + "call client streaming RPC against to gRPC-Web server": { + commonFlags: "--web", + args: "testdata/test.proto", + web: true, + input: []interface{}{"call ClientStreaming", "oumae", "kousaka", "kawashima", "kato", io.EOF}, + }, + "call server streaming RPC against to gRPC-Web server": { + commonFlags: "--web", + args: "testdata/test.proto", + web: true, + input: []interface{}{"call ServerStreaming", "violet"}, + }, + "call bidi streaming RPC against to gRPC-Web server": { + commonFlags: "--web", + args: "testdata/test.proto", + web: true, + input: []interface{}{"call BidiStreaming", "oumae", "kousaka", "kawashima", "kato", io.EOF}, + }, + + // show command. + + "show --help": { + args: "testdata/test.proto", + input: []interface{}{"show --help"}, + }, + "show package": { + args: "testdata/test.proto", + input: []interface{}{"show package"}, + }, + "show service": { + args: "testdata/test.proto", + input: []interface{}{"show service"}, + }, + "show message": { + args: "testdata/test.proto", + input: []interface{}{"show message"}, + }, + "show rpc": { + args: "testdata/test.proto", + input: []interface{}{"show rpc"}, + }, + "show an invalid target": { + args: "testdata/test.proto", + input: []interface{}{"show foo"}, + skipGolden: true, + hasErr: true, + }, + + // package command. + + "select a package": { + args: "testdata/test.proto", + input: []interface{}{"package api"}, + skipGolden: true, + }, + "specify an invalid package name": { + args: "testdata/test.proto", + input: []interface{}{"package foo"}, + skipGolden: true, + hasErr: true, + }, + + // service command. + + "select a service": { + args: "testdata/test.proto", + input: []interface{}{"service Example"}, + skipGolden: true, + }, + "specify an invalid service name": { + args: "testdata/test.proto", + input: []interface{}{"service foo"}, + skipGolden: true, + hasErr: true, + }, + + // header command. + + "add a header": { + args: "testdata/test.proto", + input: []interface{}{"header mizore=yoroizuka", "show header"}, + }, + + "add two headers": { + args: "testdata/test.proto", + input: []interface{}{"header mizore=yoroizuka nozomi=kasaki", "show header"}, + }, + "add two values to a key": { + args: "testdata/test.proto", + input: []interface{}{"header touma=youko", "header touma=kazusa", "show header"}, + }, + "add two values in one command": { + args: "testdata/test.proto", + input: []interface{}{"header touma=youko,kazusa", "show header"}, + }, + "remove a header": { + args: "testdata/test.proto", + input: []interface{}{"header grpc-client", "show header"}, + }, + + // desc command. + + "desc simple message": { + args: "testdata/test.proto", + input: []interface{}{"desc SimpleRequest"}, + }, + "desc a repeated message": { + args: "testdata/test.proto", + input: []interface{}{"desc UnaryRepeatedMessageRequest"}, + }, + "desc a map": { + args: "testdata/test.proto", + input: []interface{}{"desc UnaryMapMessageRequest"}, + }, + "desc an invalid message": { + args: "testdata/test.proto", + input: []interface{}{"desc foo"}, + skipGolden: true, + hasErr: true, + }, + + // quit command. + + "quit executes exit": { + args: "testdata/test.proto", + input: []interface{}{"quit"}, + skipGolden: true, + }, + + // special keys. + + "ctrl-c skips the rest of fields if there are no message type fields": { + args: "testdata/test.proto", + input: []interface{}{"call Unary", prompt.ErrAbort}, + }, + "ctrl-c skips the rest of the current message": { + args: "testdata/test.proto", + input: []interface{}{"call UnaryMessage", "mumei", prompt.ErrAbort}, + }, + "ctrl-c skips the rest of the current message and exits the repeated field": { + args: "testdata/test.proto", + input: []interface{}{"call UnaryRepeatedMessage", "kanade", "hisaishi", "kumiko", prompt.ErrAbort}, + }, + "ctrl-c is also enabled in streaming RPCs": { + args: "testdata/test.proto", + input: []interface{}{"call BidiStreaming", "kanade", "ririka", prompt.ErrAbort, io.EOF}, + }, + } + oldNewPrompt := prompt.New + defer func() { + prompt.New = oldNewPrompt + }() + + for name, c := range cases { + c := c + t.Run(name, func(t *testing.T) { + stopServer, port := startServer(t, c.tls, c.reflection, c.web) + defer stopServer() + + stubPrompt := &stubPrompt{ + t: t, + Prompt: oldNewPrompt(), + input: append(c.input, "exit"), + } + prompt.New = func(...prompt.Option) prompt.Prompt { + return stubPrompt + } + + args := commonFlags + args = append([]string{"--port", port}, args...) + if c.commonFlags != "" { + args = append(args, strings.Split(c.commonFlags, " ")...) + } + args = append(args, "repl") // Sub-command name. + if c.args != "" { + args = append(args, strings.Split(c.args, " ")...) + } + + w, ew := new(bytes.Buffer), new(bytes.Buffer) + ui := cui.New(cui.Writer(w), cui.ErrWriter(ew)) + + a := app.New(ui) + code := a.Run(args) + if code != c.expectedCode { + t.Errorf("unexpected code returned: expected = %d, actual = %d", c.expectedCode, code) + } + + if !c.skipGolden { + compareWithGolden(t, w.String()) + } + + if c.hasErr { + if ew.String() == "" { + t.Errorf("expected REPL wrote some error to ew, but empty output") + } + } else { + if ew.String() != "" { + t.Errorf("expected REPL didn't write errors to ew, but got '%s'", ew.String()) + } + } + }) + } +} diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-add_a_header.golden b/e2e/testdata/fixtures/teste2e_oldrepl-add_a_header.golden new file mode 100644 index 00000000..54804740 --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-add_a_header.golden @@ -0,0 +1,8 @@ + ++-------------+-----------+ +| KEY | VAL | ++-------------+-----------+ +| grpc-client | evans | +| mizore | yoroizuka | ++-------------+-----------+ + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-add_two_headers.golden b/e2e/testdata/fixtures/teste2e_oldrepl-add_two_headers.golden new file mode 100644 index 00000000..9860cdb9 --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-add_two_headers.golden @@ -0,0 +1,9 @@ + ++-------------+-----------+ +| KEY | VAL | ++-------------+-----------+ +| grpc-client | evans | +| mizore | yoroizuka | +| nozomi | kasaki | ++-------------+-----------+ + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-add_two_values_in_one_command.golden b/e2e/testdata/fixtures/teste2e_oldrepl-add_two_values_in_one_command.golden new file mode 100644 index 00000000..7b32fdc6 --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-add_two_values_in_one_command.golden @@ -0,0 +1,9 @@ + ++-------------+--------+ +| KEY | VAL | ++-------------+--------+ +| grpc-client | evans | +| touma | youko | +| touma | kazusa | ++-------------+--------+ + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-add_two_values_to_a_key.golden b/e2e/testdata/fixtures/teste2e_oldrepl-add_two_values_to_a_key.golden new file mode 100644 index 00000000..b3e26c89 --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-add_two_values_to_a_key.golden @@ -0,0 +1,10 @@ + + ++-------------+--------+ +| KEY | VAL | ++-------------+--------+ +| grpc-client | evans | +| touma | youko | +| touma | kazusa | ++-------------+--------+ + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-call_bidi_streaming_rpc_against_to_grpc-web_server.golden b/e2e/testdata/fixtures/teste2e_oldrepl-call_bidi_streaming_rpc_against_to_grpc-web_server.golden new file mode 100644 index 00000000..943ae2ba --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-call_bidi_streaming_rpc_against_to_grpc-web_server.golden @@ -0,0 +1,37 @@ +{ + "message": "hello oumae, I greet 1 times." +} +{ + "message": "hello oumae, I greet 2 times." +} +{ + "message": "hello oumae, I greet 3 times." +} +{ + "message": "hello kousaka, I greet 1 times." +} +{ + "message": "hello kousaka, I greet 2 times." +} +{ + "message": "hello kousaka, I greet 3 times." +} +{ + "message": "hello kawashima, I greet 1 times." +} +{ + "message": "hello kawashima, I greet 2 times." +} +{ + "message": "hello kawashima, I greet 3 times." +} +{ + "message": "hello kato, I greet 1 times." +} +{ + "message": "hello kato, I greet 2 times." +} +{ + "message": "hello kato, I greet 3 times." +} + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-call_bidistreaming.golden b/e2e/testdata/fixtures/teste2e_oldrepl-call_bidistreaming.golden new file mode 100644 index 00000000..6b136822 --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-call_bidistreaming.golden @@ -0,0 +1,28 @@ +{ + "message": "hello kaguya, I greet 1 times." +} +{ + "message": "hello kaguya, I greet 2 times." +} +{ + "message": "hello kaguya, I greet 3 times." +} +{ + "message": "hello chika, I greet 1 times." +} +{ + "message": "hello chika, I greet 2 times." +} +{ + "message": "hello chika, I greet 3 times." +} +{ + "message": "hello miko, I greet 1 times." +} +{ + "message": "hello miko, I greet 2 times." +} +{ + "message": "hello miko, I greet 3 times." +} + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-call_client_streaming_rpc_against_to_grpc-web_server.golden b/e2e/testdata/fixtures/teste2e_oldrepl-call_client_streaming_rpc_against_to_grpc-web_server.golden new file mode 100644 index 00000000..730ef5aa --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-call_client_streaming_rpc_against_to_grpc-web_server.golden @@ -0,0 +1,4 @@ +{ + "message": "you sent requests 4 times (oumae, kousaka, kawashima, kato)." +} + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-call_clientstreaming.golden b/e2e/testdata/fixtures/teste2e_oldrepl-call_clientstreaming.golden new file mode 100644 index 00000000..8ab1d200 --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-call_clientstreaming.golden @@ -0,0 +1,4 @@ +{ + "message": "you sent requests 3 times (kaguya, chika, miko)." +} + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-call_server_streaming_rpc_against_to_grpc-web_server.golden b/e2e/testdata/fixtures/teste2e_oldrepl-call_server_streaming_rpc_against_to_grpc-web_server.golden new file mode 100644 index 00000000..afbfcda9 --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-call_server_streaming_rpc_against_to_grpc-web_server.golden @@ -0,0 +1,10 @@ +{ + "message": "hello violet, I greet 1 times." +} +{ + "message": "hello violet, I greet 2 times." +} +{ + "message": "hello violet, I greet 3 times." +} + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-call_unary_by_selecting_only_service.golden b/e2e/testdata/fixtures/teste2e_oldrepl-call_unary_by_selecting_only_service.golden new file mode 100644 index 00000000..ac30482c --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-call_unary_by_selecting_only_service.golden @@ -0,0 +1,5 @@ + +{ + "message": "hello, kaguya" +} + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-call_unary_by_selecting_package_and_service.golden b/e2e/testdata/fixtures/teste2e_oldrepl-call_unary_by_selecting_package_and_service.golden new file mode 100644 index 00000000..8dbdc72b --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-call_unary_by_selecting_package_and_service.golden @@ -0,0 +1,6 @@ + + +{ + "message": "hello, kaguya" +} + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-call_unary_by_specifying_--service.golden b/e2e/testdata/fixtures/teste2e_oldrepl-call_unary_by_specifying_--service.golden new file mode 100644 index 00000000..3cbedde6 --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-call_unary_by_specifying_--service.golden @@ -0,0 +1,4 @@ +{ + "message": "hello, kaguya" +} + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-call_unarybytes.golden b/e2e/testdata/fixtures/teste2e_oldrepl-call_unarybytes.golden new file mode 100644 index 00000000..62e67d9e --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-call_unarybytes.golden @@ -0,0 +1,4 @@ +{ + "message": "received: (bytes) e3 82 84 e3 81 af e3 82 8a e4 bf ba e3 81 ae e9 9d 92 e6 98 a5 e3 83 a9 e3 83 96 e3 82 b3 e3 83 a1 e3 81 af e3 81 be e3 81 a1 e3 81 8c e3 81 a3 e3 81 a6 e3 81 84 e3 82 8b e3 80 82, (string) やはり俺の青春ラブコメはまちがっている。" +} + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-call_unaryenum.golden b/e2e/testdata/fixtures/teste2e_oldrepl-call_unaryenum.golden new file mode 100644 index 00000000..4d55479f --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-call_unaryenum.golden @@ -0,0 +1,4 @@ +{ + "message": "M" +} + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-call_unarymessage.golden b/e2e/testdata/fixtures/teste2e_oldrepl-call_unarymessage.golden new file mode 100644 index 00000000..c735239d --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-call_unarymessage.golden @@ -0,0 +1,4 @@ +{ + "message": "hello, kaguya shinomiya" +} + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-call_unaryoneof.golden b/e2e/testdata/fixtures/teste2e_oldrepl-call_unaryoneof.golden new file mode 100644 index 00000000..01f8951b --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-call_unaryoneof.golden @@ -0,0 +1,4 @@ +{ + "message": "hello, ai hayasaka" +} + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-call_unaryrepeated.golden b/e2e/testdata/fixtures/teste2e_oldrepl-call_unaryrepeated.golden new file mode 100644 index 00000000..000101d1 --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-call_unaryrepeated.golden @@ -0,0 +1,4 @@ +{ + "message": "hello, miyuki, kaguya, chika, yu" +} + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-call_unaryrepeatedenum.golden b/e2e/testdata/fixtures/teste2e_oldrepl-call_unaryrepeatedenum.golden new file mode 100644 index 00000000..6608712a --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-call_unaryrepeatedenum.golden @@ -0,0 +1,4 @@ +{ + "message": "M: 2, F:1" +} + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-call_unaryself.golden b/e2e/testdata/fixtures/teste2e_oldrepl-call_unaryself.golden new file mode 100644 index 00000000..d3d28c39 --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-call_unaryself.golden @@ -0,0 +1,4 @@ +{ + "message": "ohana matsumae (ohana), friends: nakochi, minchi → nako oshimizu (nakochi), friends: → minko tsurugi (minchi), friends: → " +} + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-ctrl-c_is_also_enabled_in_streaming_rpcs.golden b/e2e/testdata/fixtures/teste2e_oldrepl-ctrl-c_is_also_enabled_in_streaming_rpcs.golden new file mode 100644 index 00000000..0bc4055e --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-ctrl-c_is_also_enabled_in_streaming_rpcs.golden @@ -0,0 +1,28 @@ +{ + "message": "hello kanade, I greet 1 times." +} +{ + "message": "hello kanade, I greet 2 times." +} +{ + "message": "hello kanade, I greet 3 times." +} +{ + "message": "hello ririka, I greet 1 times." +} +{ + "message": "hello ririka, I greet 2 times." +} +{ + "message": "hello ririka, I greet 3 times." +} +{ + "message": "hello , I greet 1 times." +} +{ + "message": "hello , I greet 2 times." +} +{ + "message": "hello , I greet 3 times." +} + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-ctrl-c_skips_the_rest_of_fields_if_there_are_no_message_type_fields.golden b/e2e/testdata/fixtures/teste2e_oldrepl-ctrl-c_skips_the_rest_of_fields_if_there_are_no_message_type_fields.golden new file mode 100644 index 00000000..5915f4e2 --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-ctrl-c_skips_the_rest_of_fields_if_there_are_no_message_type_fields.golden @@ -0,0 +1,4 @@ +{ + "message": "hello, " +} + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-ctrl-c_skips_the_rest_of_the_current_message.golden b/e2e/testdata/fixtures/teste2e_oldrepl-ctrl-c_skips_the_rest_of_the_current_message.golden new file mode 100644 index 00000000..c7406f6a --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-ctrl-c_skips_the_rest_of_the_current_message.golden @@ -0,0 +1,4 @@ +{ + "message": "hello, mumei " +} + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-ctrl-c_skips_the_rest_of_the_current_message_and_exits_the_repeated_field.golden b/e2e/testdata/fixtures/teste2e_oldrepl-ctrl-c_skips_the_rest_of_the_current_message_and_exits_the_repeated_field.golden new file mode 100644 index 00000000..e2ca8fa4 --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-ctrl-c_skips_the_rest_of_the_current_message_and_exits_the_repeated_field.golden @@ -0,0 +1,4 @@ +{ + "message": "hello, kanade hisaishi, kumiko " +} + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-desc_a_map.golden b/e2e/testdata/fixtures/teste2e_oldrepl-desc_a_map.golden new file mode 100644 index 00000000..2c218562 --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-desc_a_map.golden @@ -0,0 +1,7 @@ ++-------+--------------------------------+----------+ +| FIELD | TYPE | REPEATED | ++-------+--------------------------------+----------+ +| kvs | map | | ++-------+--------------------------------+----------+ + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-desc_a_repeated_message.golden b/e2e/testdata/fixtures/teste2e_oldrepl-desc_a_repeated_message.golden new file mode 100644 index 00000000..60731141 --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-desc_a_repeated_message.golden @@ -0,0 +1,6 @@ ++-------+---------------------+----------+ +| FIELD | TYPE | REPEATED | ++-------+---------------------+----------+ +| name | TYPE_MESSAGE (Name) | true | ++-------+---------------------+----------+ + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-desc_simple_message.golden b/e2e/testdata/fixtures/teste2e_oldrepl-desc_simple_message.golden new file mode 100644 index 00000000..1b3f6537 --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-desc_simple_message.golden @@ -0,0 +1,6 @@ ++-------+-------------+----------+ +| FIELD | TYPE | REPEATED | ++-------+-------------+----------+ +| name | TYPE_STRING | false | ++-------+-------------+----------+ + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-remove_a_header.golden b/e2e/testdata/fixtures/teste2e_oldrepl-remove_a_header.golden new file mode 100644 index 00000000..d93af101 --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-remove_a_header.golden @@ -0,0 +1,6 @@ + ++-----+-----+ +| KEY | VAL | ++-----+-----+ ++-----+-----+ + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-show_--help.golden b/e2e/testdata/fixtures/teste2e_oldrepl-show_--help.golden new file mode 100644 index 00000000..1a6d25ad --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-show_--help.golden @@ -0,0 +1,2 @@ +usage: show + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-show_message.golden b/e2e/testdata/fixtures/teste2e_oldrepl-show_message.golden new file mode 100644 index 00000000..c1b12756 --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-show_message.golden @@ -0,0 +1,18 @@ ++-----------------------------+ +| MESSAGE | ++-----------------------------+ +| SimpleRequest | +| SimpleResponse | +| UnaryBytesRequest | +| UnaryEnumRequest | +| UnaryHeaderRequest | +| UnaryMapMessageRequest | +| UnaryMapRequest | +| UnaryMessageRequest | +| UnaryOneofRequest | +| UnaryRepeatedEnumRequest | +| UnaryRepeatedMessageRequest | +| UnaryRepeatedRequest | +| UnarySelfRequest | ++-----------------------------+ + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-show_package.golden b/e2e/testdata/fixtures/teste2e_oldrepl-show_package.golden new file mode 100644 index 00000000..f831f286 --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-show_package.golden @@ -0,0 +1,6 @@ ++---------+ +| PACKAGE | ++---------+ +| api | ++---------+ + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-show_rpc.golden b/e2e/testdata/fixtures/teste2e_oldrepl-show_rpc.golden new file mode 100644 index 00000000..9bed7a76 --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-show_rpc.golden @@ -0,0 +1,20 @@ ++----------------------+-----------------------------+----------------+ +| RPC | REQUEST TYPE | RESPONSE TYPE | ++----------------------+-----------------------------+----------------+ +| BidiStreaming | SimpleRequest | SimpleResponse | +| ClientStreaming | SimpleRequest | SimpleResponse | +| ServerStreaming | SimpleRequest | SimpleResponse | +| Unary | SimpleRequest | SimpleResponse | +| UnaryBytes | UnaryBytesRequest | SimpleResponse | +| UnaryEnum | UnaryEnumRequest | SimpleResponse | +| UnaryHeader | UnaryHeaderRequest | SimpleResponse | +| UnaryMap | UnaryMapRequest | SimpleResponse | +| UnaryMapMessage | UnaryMapMessageRequest | SimpleResponse | +| UnaryMessage | UnaryMessageRequest | SimpleResponse | +| UnaryOneof | UnaryOneofRequest | SimpleResponse | +| UnaryRepeated | UnaryRepeatedRequest | SimpleResponse | +| UnaryRepeatedEnum | UnaryRepeatedEnumRequest | SimpleResponse | +| UnaryRepeatedMessage | UnaryRepeatedMessageRequest | SimpleResponse | +| UnarySelf | UnarySelfRequest | SimpleResponse | ++----------------------+-----------------------------+----------------+ + diff --git a/e2e/testdata/fixtures/teste2e_oldrepl-show_service.golden b/e2e/testdata/fixtures/teste2e_oldrepl-show_service.golden new file mode 100644 index 00000000..ffabe1da --- /dev/null +++ b/e2e/testdata/fixtures/teste2e_oldrepl-show_service.golden @@ -0,0 +1,20 @@ ++---------+----------------------+-----------------------------+----------------+ +| SERVICE | RPC | REQUEST TYPE | RESPONSE TYPE | ++---------+----------------------+-----------------------------+----------------+ +| Example | Unary | SimpleRequest | SimpleResponse | +| Example | UnaryMessage | UnaryMessageRequest | SimpleResponse | +| Example | UnaryRepeated | UnaryRepeatedRequest | SimpleResponse | +| Example | UnaryRepeatedMessage | UnaryRepeatedMessageRequest | SimpleResponse | +| Example | UnaryRepeatedEnum | UnaryRepeatedEnumRequest | SimpleResponse | +| Example | UnarySelf | UnarySelfRequest | SimpleResponse | +| Example | UnaryMap | UnaryMapRequest | SimpleResponse | +| Example | UnaryMapMessage | UnaryMapMessageRequest | SimpleResponse | +| Example | UnaryOneof | UnaryOneofRequest | SimpleResponse | +| Example | UnaryEnum | UnaryEnumRequest | SimpleResponse | +| Example | UnaryBytes | UnaryBytesRequest | SimpleResponse | +| Example | UnaryHeader | UnaryHeaderRequest | SimpleResponse | +| Example | ClientStreaming | SimpleRequest | SimpleResponse | +| Example | ServerStreaming | SimpleRequest | SimpleResponse | +| Example | BidiStreaming | SimpleRequest | SimpleResponse | ++---------+----------------------+-----------------------------+----------------+ + From fbee6dce2db054c866f227385b6501e6fedb242c Mon Sep 17 00:00:00 2001 From: ktr0731 Date: Sun, 22 Dec 2019 18:46:43 +0900 Subject: [PATCH 12/12] fix old CLI and REPL e2e tests --- app/commands.go | 22 ++++++++++++++++++++-- cui/ui.go | 11 +++++++++++ e2e/old_cli_test.go | 12 ++++++++++-- e2e/old_repl_test.go | 8 ++++++-- 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/app/commands.go b/app/commands.go index 7df23443..564a89f8 100644 --- a/app/commands.go +++ b/app/commands.go @@ -91,6 +91,8 @@ func newOldCommand(flags *flags, ui cui.UI) *command { ui = cui.NewColored(ui) } + defer ui.Warn("evans: deprecated usage, please use sub-commands. see `evans -h` for more details.") + isCLIMode := (cfg.cli || mode.IsCLIMode(cfg.file)) if cfg.repl || !isCLIMode { cache, err := cache.Get() @@ -136,6 +138,20 @@ func newOldCommand(flags *flags, ui cui.UI) *command { bindFlags(cmd.PersistentFlags(), flags, ui.Writer()) cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { cmd.PersistentFlags().Usage() + // If Evans is launched as old-style, hidden sub-command help. + if len(cmd.Commands()) > 0 { + fmt.Fprintf(ui.Writer(), "Available Commands:\n") + w := tabwriter.NewWriter(ui.Writer(), 0, 8, 8, ' ', tabwriter.TabIndent) + for _, c := range cmd.Commands() { + // Ignore help command. + if c.Name() == "help" { + continue + } + fmt.Fprintf(w, " %s\t%s\n", c.Name(), c.Short) + } + w.Flush() + fmt.Fprintf(ui.Writer(), "\n") + } }) cmd.SetOut(ui.Writer()) return &command{cmd, flags, ui} @@ -190,7 +206,8 @@ func bindFlags(f *pflag.FlagSet, flags *flags, w io.Writer) { func newCLICommand(flags *flags, ui cui.UI) *cobra.Command { cmd := &cobra.Command{ - Use: "cli", + Use: "cli", + Short: "CLI mode", RunE: runFunc(flags, func(_ *cobra.Command, cfg *mergedConfig) error { if err := mode.RunAsCLIMode(cfg.Config, cfg.call, cfg.file, ui); err != nil { return errors.Wrap(err, "failed to run CLI mode") @@ -209,7 +226,8 @@ func newCLICommand(flags *flags, ui cui.UI) *cobra.Command { func newREPLCommand(flags *flags, ui cui.UI) *cobra.Command { cmd := &cobra.Command{ - Use: "repl", + Use: "repl", + Short: "REPL mode", RunE: runFunc(flags, func(_ *cobra.Command, cfg *mergedConfig) error { cache, err := cache.Get() if err != nil { diff --git a/cui/ui.go b/cui/ui.go index 28f44a16..632e0dae 100644 --- a/cui/ui.go +++ b/cui/ui.go @@ -14,6 +14,7 @@ import ( type UI interface { Output(s string) Info(s string) + Warn(s string) Error(s string) Writer() io.Writer @@ -46,6 +47,11 @@ func (u *basicUI) Info(s string) { u.Output(s) } +// Warn is the same as Output, but distinguish these for composition. +func (u *basicUI) Warn(s string) { + u.Error(s) +} + // Error writes out the passed argument s to ErrWriter with a line break. func (u *basicUI) Error(s string) { fmt.Fprintln(u.errWriter, s) @@ -75,6 +81,11 @@ func (u *coloredUI) Info(s string) { u.UI.Info(color.BlueString(s)) } +// Warn is the same as New, but colored. +func (u *coloredUI) Warn(s string) { + u.UI.Warn(color.YellowString(s)) +} + // Error is the same as New, but colored. func (u *coloredUI) Error(s string) { u.UI.Error(color.RedString(s)) diff --git a/e2e/old_cli_test.go b/e2e/old_cli_test.go index 26b612a6..a1610edc 100644 --- a/e2e/old_cli_test.go +++ b/e2e/old_cli_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/fatih/color" "github.com/google/go-cmp/cmp" "github.com/ktr0731/evans/app" "github.com/ktr0731/evans/cui" @@ -341,8 +342,11 @@ func TestE2E_OldCLI(t *testing.T) { if c.expectedOut != "" && actual != c.expectedOut { t.Errorf("unexpected output:\n%s", cmp.Diff(c.expectedOut, actual)) } - if eoutBuf.String() != "" { - t.Errorf("expected code is 0, but got an error message: '%s'", eoutBuf.String()) + eout := eoutBuf.String() + // Trim "deprecated" message. + eout = strings.Replace(eout, color.YellowString("evans: deprecated usage, please use sub-commands. see `evans -h` for more details.")+"\n", "", -1) + if eout != "" { + t.Errorf("expected code is 0, but got an error message: '%s'", eout) } } }) @@ -377,4 +381,8 @@ Options: --version, -v display version and exit (default "false") --help, -h display help text and exit (default "false") +Available Commands: + cli CLI mode + repl REPL mode + `, meta.Version) diff --git a/e2e/old_repl_test.go b/e2e/old_repl_test.go index c3f22c50..bd38c12e 100644 --- a/e2e/old_repl_test.go +++ b/e2e/old_repl_test.go @@ -11,6 +11,7 @@ import ( "strings" "testing" + "github.com/fatih/color" "github.com/google/go-cmp/cmp" "github.com/ktr0731/evans/app" "github.com/ktr0731/evans/cui" @@ -309,8 +310,11 @@ func TestE2E_OldREPL(t *testing.T) { t.Errorf("expected REPL wrote some error to ew, but empty output") } } else { - if ew.String() != "" { - t.Errorf("expected REPL didn't write errors to ew, but got '%s'", ew.String()) + eout := ew.String() + // Trim "deprecated" message. + eout = strings.Replace(eout, color.YellowString("evans: deprecated usage, please use sub-commands. see `evans -h` for more details.")+"\n", "", -1) + if eout != "" { + t.Errorf("expected REPL didn't write errors to ew, but got '%s'", eout) } } })