From 1b83ee58c916d11c52113fff647d098e1bb082d9 Mon Sep 17 00:00:00 2001 From: "shhsu@microsoft.com" Date: Tue, 30 May 2017 14:36:15 -0700 Subject: [PATCH 01/13] Silent login: if user did not provide -u and -p flag for login command but both username and password are retrieved in cred store, docker will automatically use the credentials found in the cred store to log in Signed-off-by: shhsu@microsoft.com Signed-off-by: Peter Hsu Signed-off-by: shhsu Signed-off-by: Peter Hsu Upstream-commit: 8883cd636a91f7c38f31005c09d14cfd6fe25a92 Component: cli --- components/cli/cli/command/registry.go | 49 +++--- components/cli/cli/command/registry/login.go | 55 +++++-- .../cli/cli/command/registry/login_test.go | 151 ++++++++++++++++++ components/cli/cli/command/registry_test.go | 71 ++++++++ components/cli/internal/test/store.go | 30 ++-- 5 files changed, 313 insertions(+), 43 deletions(-) create mode 100644 components/cli/cli/command/registry/login_test.go diff --git a/components/cli/cli/command/registry.go b/components/cli/cli/command/registry.go index f6e5ac4659..cb9a3947da 100644 --- a/components/cli/cli/command/registry.go +++ b/components/cli/cli/command/registry.go @@ -52,11 +52,15 @@ func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInf fmt.Fprintf(cli.Out(), "\nPlease login prior to %s:\n", cmdName) indexServer := registry.GetAuthConfigKey(index) isDefaultRegistry := indexServer == ElectAuthServer(context.Background(), cli) - authConfig, err := ConfigureAuth(cli, "", "", indexServer, isDefaultRegistry) + authConfig, err := GetDefaultAuthConfig(cli, true, indexServer, isDefaultRegistry) + if err != nil { + fmt.Fprintf(cli.Err(), "Unable to retrieve stored credentials for %s, error: %s.\n", indexServer, err) + } + err = ConfigureAuth(cli, "", "", authConfig, isDefaultRegistry) if err != nil { return "", err } - return EncodeAuthToBase64(authConfig) + return EncodeAuthToBase64(*authConfig) } } @@ -73,20 +77,29 @@ func ResolveAuthConfig(ctx context.Context, cli Cli, index *registrytypes.IndexI return a } -// ConfigureAuth returns an AuthConfig from the specified user, password and server. -func ConfigureAuth(cli Cli, flUser, flPassword, serverAddress string, isDefaultRegistry bool) (types.AuthConfig, error) { - // On Windows, force the use of the regular OS stdin stream. Fixes #14336/#14210 - if runtime.GOOS == "windows" { - cli.SetIn(NewInStream(os.Stdin)) - } - +// GetDefaultAuthConfig gets the default auth config given a serverAddress +// If credentials for given serverAddress exists in the credential store, the configuration will be populated with values in it +func GetDefaultAuthConfig(cli Cli, checkCredStore bool, serverAddress string, isDefaultRegistry bool) (*types.AuthConfig, error) { if !isDefaultRegistry { serverAddress = registry.ConvertToHostname(serverAddress) } + var authconfig types.AuthConfig + var err error + if checkCredStore { + authconfig, err = cli.ConfigFile().GetAuthConfig(serverAddress) + } else { + authconfig = types.AuthConfig{} + } + authconfig.ServerAddress = serverAddress + authconfig.IdentityToken = "" + return &authconfig, err +} - authconfig, err := cli.ConfigFile().GetAuthConfig(serverAddress) - if err != nil { - return authconfig, err +// ConfigureAuth handles prompting of user's username and password if needed +func ConfigureAuth(cli Cli, flUser, flPassword string, authconfig *types.AuthConfig, isDefaultRegistry bool) error { + // On Windows, force the use of the regular OS stdin stream. Fixes #14336/#14210 + if runtime.GOOS == "windows" { + cli.SetIn(NewInStream(os.Stdin)) } // Some links documenting this: @@ -97,7 +110,7 @@ func ConfigureAuth(cli Cli, flUser, flPassword, serverAddress string, isDefaultR // will hit this if you attempt docker login from mintty where stdin // is a pipe, not a character based console. if flPassword == "" && !cli.In().IsTerminal() { - return authconfig, errors.Errorf("Error: Cannot perform an interactive login from a non TTY device") + return errors.Errorf("Error: Cannot perform an interactive login from a non TTY device") } authconfig.Username = strings.TrimSpace(authconfig.Username) @@ -115,12 +128,12 @@ func ConfigureAuth(cli Cli, flUser, flPassword, serverAddress string, isDefaultR } } if flUser == "" { - return authconfig, errors.Errorf("Error: Non-null Username Required") + return errors.Errorf("Error: Non-null Username Required") } if flPassword == "" { oldState, err := term.SaveState(cli.In().FD()) if err != nil { - return authconfig, err + return err } fmt.Fprintf(cli.Out(), "Password: ") term.DisableEcho(cli.In().FD(), oldState) @@ -130,16 +143,14 @@ func ConfigureAuth(cli Cli, flUser, flPassword, serverAddress string, isDefaultR term.RestoreTerminal(cli.In().FD(), oldState) if flPassword == "" { - return authconfig, errors.Errorf("Error: Password Required") + return errors.Errorf("Error: Password Required") } } authconfig.Username = flUser authconfig.Password = flPassword - authconfig.ServerAddress = serverAddress - authconfig.IdentityToken = "" - return authconfig, nil + return nil } func readInput(in io.Reader, out io.Writer) string { diff --git a/components/cli/cli/command/registry/login.go b/components/cli/cli/command/registry/login.go index 938fef8ab7..b538fb0084 100644 --- a/components/cli/cli/command/registry/login.go +++ b/components/cli/cli/command/registry/login.go @@ -9,6 +9,9 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/docker/api/types" + registrytypes "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/client" "github.com/docker/docker/registry" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -47,10 +50,7 @@ func NewLoginCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runLogin(dockerCli command.Cli, opts loginOptions) error { - ctx := context.Background() - clnt := dockerCli.Client() - +func verifyloginOptions(dockerCli command.Cli, opts *loginOptions) error { if opts.password != "" { fmt.Fprintln(dockerCli.Err(), "WARNING! Using --password via the CLI is insecure. Use --password-stdin.") if opts.passwordStdin { @@ -71,7 +71,15 @@ func runLogin(dockerCli command.Cli, opts loginOptions) error { opts.password = strings.TrimSuffix(string(contents), "\n") opts.password = strings.TrimSuffix(opts.password, "\r") } + return nil +} +func runLogin(dockerCli command.Cli, opts loginOptions) error { + ctx := context.Background() + clnt := dockerCli.Client() + if err := verifyloginOptions(dockerCli, &opts); err != nil { + return err + } var ( serverAddress string authServer = command.ElectAuthServer(ctx, dockerCli) @@ -82,21 +90,30 @@ func runLogin(dockerCli command.Cli, opts loginOptions) error { serverAddress = authServer } + var err error + var authConfig *types.AuthConfig + var response registrytypes.AuthenticateOKBody isDefaultRegistry := serverAddress == authServer - - authConfig, err := command.ConfigureAuth(dockerCli, opts.user, opts.password, serverAddress, isDefaultRegistry) - if err != nil { - return err + authConfig, err = command.GetDefaultAuthConfig(dockerCli, opts.user == "" && opts.password == "", serverAddress, isDefaultRegistry) + if err == nil && authConfig.Username != "" && authConfig.Password != "" { + response, err = loginWithCredStoreCreds(ctx, dockerCli, authConfig) } - response, err := clnt.RegistryLogin(ctx, authConfig) - if err != nil { - return err + if err != nil || authConfig.Username == "" || authConfig.Password == "" { + err = command.ConfigureAuth(dockerCli, opts.user, opts.password, authConfig, isDefaultRegistry) + if err != nil { + return err + } + + response, err = clnt.RegistryLogin(ctx, *authConfig) + if err != nil { + return err + } } if response.IdentityToken != "" { authConfig.Password = "" authConfig.IdentityToken = response.IdentityToken } - if err := dockerCli.ConfigFile().GetCredentialsStore(serverAddress).Store(authConfig); err != nil { + if err := dockerCli.ConfigFile().GetCredentialsStore(serverAddress).Store(*authConfig); err != nil { return errors.Errorf("Error saving credentials: %v", err) } @@ -105,3 +122,17 @@ func runLogin(dockerCli command.Cli, opts loginOptions) error { } return nil } + +func loginWithCredStoreCreds(ctx context.Context, dockerCli command.Cli, authConfig *types.AuthConfig) (registrytypes.AuthenticateOKBody, error) { + fmt.Fprintf(dockerCli.Out(), "Authenticating with existing credentials...\n") + cliClient := dockerCli.Client() + response, err := cliClient.RegistryLogin(ctx, *authConfig) + if err != nil { + if client.IsErrUnauthorized(err) { + fmt.Fprintf(dockerCli.Err(), "Stored credentials invalid or expired\n") + } else { + fmt.Fprintf(dockerCli.Err(), "Login did not succeed, error: %s\n", err) + } + } + return response, err +} diff --git a/components/cli/cli/command/registry/login_test.go b/components/cli/cli/command/registry/login_test.go new file mode 100644 index 0000000000..498e0267f2 --- /dev/null +++ b/components/cli/cli/command/registry/login_test.go @@ -0,0 +1,151 @@ +package registry + +import ( + "bytes" + "fmt" + "testing" + + "golang.org/x/net/context" + + "github.com/docker/cli/internal/test" + "github.com/docker/docker/api/types" + registrytypes "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/client" + "github.com/stretchr/testify/assert" +) + +const userErr = "userunknownError" +const testAuthErrMsg = "UNKNOWN_ERR" + +var testAuthErrors = map[string]error{ + userErr: fmt.Errorf(testAuthErrMsg), +} + +var expiredPassword = "I_M_EXPIRED" + +type fakeClient struct { + client.Client +} + +// nolint: unparam +func (c fakeClient) RegistryLogin(ctx context.Context, auth types.AuthConfig) (registrytypes.AuthenticateOKBody, error) { + if auth.Password == expiredPassword { + return registrytypes.AuthenticateOKBody{}, fmt.Errorf("Invalid Username or Password") + } + err := testAuthErrors[auth.Username] + return registrytypes.AuthenticateOKBody{}, err +} + +func TestLoginWithCredStoreCreds(t *testing.T) { + testCases := []struct { + inputAuthConfig types.AuthConfig + expectedMsg string + expectedErr string + }{ + { + inputAuthConfig: types.AuthConfig{}, + expectedMsg: "Authenticating with existing credentials...\n", + }, + { + inputAuthConfig: types.AuthConfig{ + Username: userErr, + }, + expectedMsg: "Authenticating with existing credentials...\n", + expectedErr: fmt.Sprintf("Login did not succeed, error: %s\n", testAuthErrMsg), + }, + // can't easily test the 401 case because client.IsErrUnauthorized(err) involving + // creating an error of a private type + } + ctx := context.Background() + for _, tc := range testCases { + cli := (*test.FakeCli)(test.NewFakeCli(&fakeClient{})) + errBuf := new(bytes.Buffer) + cli.SetErr(errBuf) + loginWithCredStoreCreds(ctx, cli, &tc.inputAuthConfig) + outputString := cli.OutBuffer().String() + assert.Equal(t, tc.expectedMsg, outputString) + errorString := errBuf.String() + assert.Equal(t, tc.expectedErr, errorString) + } +} + +func TestRunLogin(t *testing.T) { + const storedServerAddress = "reg1" + const validUsername = "u1" + const validPassword = "p1" + const validPassword2 = "p2" + + validAuthConfig := types.AuthConfig{ + ServerAddress: storedServerAddress, + Username: validUsername, + Password: validPassword, + } + expiredAuthConfig := types.AuthConfig{ + ServerAddress: storedServerAddress, + Username: validUsername, + Password: expiredPassword, + } + testCases := []struct { + inputLoginOption loginOptions + inputStoredCred *types.AuthConfig + expectedErr string + expectedSavedCred types.AuthConfig + }{ + { + inputLoginOption: loginOptions{ + serverAddress: storedServerAddress, + }, + inputStoredCred: &validAuthConfig, + expectedErr: "", + expectedSavedCred: validAuthConfig, + }, + { + inputLoginOption: loginOptions{ + serverAddress: storedServerAddress, + }, + inputStoredCred: &expiredAuthConfig, + expectedErr: "Error: Cannot perform an interactive login from a non TTY device", + }, + { + inputLoginOption: loginOptions{ + serverAddress: storedServerAddress, + user: validUsername, + password: validPassword2, + }, + inputStoredCred: &validAuthConfig, + expectedErr: "", + expectedSavedCred: types.AuthConfig{ + ServerAddress: storedServerAddress, + Username: validUsername, + Password: validPassword2, + }, + }, + { + inputLoginOption: loginOptions{ + serverAddress: storedServerAddress, + user: userErr, + password: validPassword, + }, + inputStoredCred: &validAuthConfig, + expectedErr: testAuthErrMsg, + }, + } + for _, tc := range testCases { + cli := test.NewFakeCli(&fakeClient{}) + errBuf := new(bytes.Buffer) + cli.SetErr(errBuf) + if tc.inputStoredCred != nil { + cred := *tc.inputStoredCred + cli.ConfigFile().GetCredentialsStore(cred.ServerAddress).Store(cred) + } + loginErr := runLogin(cli, tc.inputLoginOption) + if tc.expectedErr != "" { + assert.Equal(t, tc.expectedErr, loginErr.Error()) + } else { + assert.Nil(t, loginErr) + savedCred, credStoreErr := cli.ConfigFile().GetCredentialsStore(tc.inputStoredCred.ServerAddress).Get(tc.inputStoredCred.ServerAddress) + assert.Nil(t, credStoreErr) + assert.Equal(t, tc.expectedSavedCred, savedCred) + } + } +} diff --git a/components/cli/cli/command/registry_test.go b/components/cli/cli/command/registry_test.go index 3f3d5f579f..3672ae8d45 100644 --- a/components/cli/cli/command/registry_test.go +++ b/components/cli/cli/command/registry_test.go @@ -1,6 +1,8 @@ package command_test import ( + "bytes" + "fmt" "testing" "github.com/pkg/errors" @@ -8,6 +10,7 @@ import ( "golang.org/x/net/context" // Prevents a circular import with "github.com/docker/cli/internal/test" + . "github.com/docker/cli/cli/command" "github.com/docker/cli/internal/test" "github.com/docker/docker/api/types" @@ -19,6 +22,19 @@ type fakeClient struct { infoFunc func() (types.Info, error) } +var testAuthConfigs = []types.AuthConfig{ + { + ServerAddress: "https://index.docker.io/v1/", + Username: "u0", + Password: "p0", + }, + { + ServerAddress: "server1.io", + Username: "u1", + Password: "p1", + }, +} + func (cli *fakeClient) Info(_ context.Context) (types.Info, error) { if cli.infoFunc != nil { return cli.infoFunc() @@ -73,3 +89,58 @@ func TestElectAuthServer(t *testing.T) { } } } + +func TestGetDefaultAuthConfig(t *testing.T) { + testCases := []struct { + checkCredStore bool + inputServerAddress string + expectedErr string + expectedAuthConfig types.AuthConfig + }{ + { + checkCredStore: false, + inputServerAddress: "", + expectedErr: "", + expectedAuthConfig: types.AuthConfig{ + ServerAddress: "", + Username: "", + Password: "", + }, + }, + { + checkCredStore: true, + inputServerAddress: testAuthConfigs[0].ServerAddress, + expectedErr: "", + expectedAuthConfig: testAuthConfigs[0], + }, + { + checkCredStore: true, + inputServerAddress: testAuthConfigs[1].ServerAddress, + expectedErr: "", + expectedAuthConfig: testAuthConfigs[1], + }, + { + checkCredStore: true, + inputServerAddress: fmt.Sprintf("https://%s", testAuthConfigs[1].ServerAddress), + expectedErr: "", + expectedAuthConfig: testAuthConfigs[1], + }, + } + cli := test.NewFakeCli(&fakeClient{}) + errBuf := new(bytes.Buffer) + cli.SetErr(errBuf) + for _, authconfig := range testAuthConfigs { + cli.ConfigFile().GetCredentialsStore(authconfig.ServerAddress).Store(authconfig) + } + for _, tc := range testCases { + serverAddress := tc.inputServerAddress + authconfig, err := GetDefaultAuthConfig(cli, tc.checkCredStore, serverAddress, serverAddress == "https://index.docker.io/v1/") + if tc.expectedErr != "" { + assert.NotNil(t, err) + assert.Equal(t, tc.expectedErr, err.Error()) + } else { + assert.Nil(t, err) + assert.Equal(t, tc.expectedAuthConfig, *authconfig) + } + } +} diff --git a/components/cli/internal/test/store.go b/components/cli/internal/test/store.go index e5b3de7abb..35565dc68e 100644 --- a/components/cli/internal/test/store.go +++ b/components/cli/internal/test/store.go @@ -5,8 +5,8 @@ import ( "github.com/docker/docker/api/types" ) -// fake store implements a credentials.Store that only acts as an in memory map -type fakeStore struct { +// FakeStore implements a credentials.Store that only acts as an in memory map +type FakeStore struct { store map[string]types.AuthConfig eraseFunc func(serverAddress string) error getFunc func(serverAddress string) (types.AuthConfig, error) @@ -16,31 +16,36 @@ type fakeStore struct { // NewFakeStore creates a new file credentials store. func NewFakeStore() credentials.Store { - return &fakeStore{store: map[string]types.AuthConfig{}} + return &FakeStore{store: map[string]types.AuthConfig{}} } -func (c *fakeStore) SetStore(store map[string]types.AuthConfig) { +// SetStore is used to overrides Set function +func (c *FakeStore) SetStore(store map[string]types.AuthConfig) { c.store = store } -func (c *fakeStore) SetEraseFunc(eraseFunc func(string) error) { +// SetEraseFunc is used to overrides Erase function +func (c *FakeStore) SetEraseFunc(eraseFunc func(string) error) { c.eraseFunc = eraseFunc } -func (c *fakeStore) SetGetFunc(getFunc func(string) (types.AuthConfig, error)) { +// SetGetFunc is used to overrides Get function +func (c *FakeStore) SetGetFunc(getFunc func(string) (types.AuthConfig, error)) { c.getFunc = getFunc } -func (c *fakeStore) SetGetAllFunc(getAllFunc func() (map[string]types.AuthConfig, error)) { +// SetGetAllFunc is used to overrides GetAll function +func (c *FakeStore) SetGetAllFunc(getAllFunc func() (map[string]types.AuthConfig, error)) { c.getAllFunc = getAllFunc } -func (c *fakeStore) SetStoreFunc(storeFunc func(types.AuthConfig) error) { +// SetStoreFunc is used to override Store function +func (c *FakeStore) SetStoreFunc(storeFunc func(types.AuthConfig) error) { c.storeFunc = storeFunc } // Erase removes the given credentials from the map store -func (c *fakeStore) Erase(serverAddress string) error { +func (c *FakeStore) Erase(serverAddress string) error { if c.eraseFunc != nil { return c.eraseFunc(serverAddress) } @@ -49,14 +54,15 @@ func (c *fakeStore) Erase(serverAddress string) error { } // Get retrieves credentials for a specific server from the map store. -func (c *fakeStore) Get(serverAddress string) (types.AuthConfig, error) { +func (c *FakeStore) Get(serverAddress string) (types.AuthConfig, error) { if c.getFunc != nil { return c.getFunc(serverAddress) } return c.store[serverAddress], nil } -func (c *fakeStore) GetAll() (map[string]types.AuthConfig, error) { +// GetAll returns the key value pairs of ServerAddress => Username +func (c *FakeStore) GetAll() (map[string]types.AuthConfig, error) { if c.getAllFunc != nil { return c.getAllFunc() } @@ -64,7 +70,7 @@ func (c *fakeStore) GetAll() (map[string]types.AuthConfig, error) { } // Store saves the given credentials in the map store. -func (c *fakeStore) Store(authConfig types.AuthConfig) error { +func (c *FakeStore) Store(authConfig types.AuthConfig) error { if c.storeFunc != nil { return c.storeFunc(authConfig) } From d3e338ece56ee0413f951bb8b0e99499aa223625 Mon Sep 17 00:00:00 2001 From: Silvin Lubecki Date: Thu, 22 Feb 2018 14:55:42 +0100 Subject: [PATCH 02/13] Print Stack API version in version command * Resolve Stack API using Kubernetes discovering API * Refactor Kubernetes flags parsing Signed-off-by: Silvin Lubecki Upstream-commit: 854aad8927e9e61c10be944ced7546f0b7142379 Component: cli --- components/cli/cli/command/stack/deploy.go | 2 +- .../cli/cli/command/stack/kubernetes/check.go | 32 --------- .../cli/cli/command/stack/kubernetes/cli.go | 71 +++++++++++------- components/cli/cli/command/stack/list.go | 2 +- components/cli/cli/command/stack/ps.go | 2 +- components/cli/cli/command/stack/remove.go | 2 +- components/cli/cli/command/stack/services.go | 2 +- components/cli/cli/command/system/version.go | 72 +++++++++++++++++-- components/cli/kubernetes/check.go | 50 +++++++++++++ components/cli/kubernetes/check_test.go | 49 +++++++++++++ components/cli/kubernetes/config.go | 24 +++++++ 11 files changed, 242 insertions(+), 66 deletions(-) delete mode 100644 components/cli/cli/command/stack/kubernetes/check.go create mode 100644 components/cli/kubernetes/check.go create mode 100644 components/cli/kubernetes/check_test.go create mode 100644 components/cli/kubernetes/config.go diff --git a/components/cli/cli/command/stack/deploy.go b/components/cli/cli/command/stack/deploy.go index 106dd83e8b..a21640d054 100644 --- a/components/cli/cli/command/stack/deploy.go +++ b/components/cli/cli/command/stack/deploy.go @@ -20,7 +20,7 @@ func newDeployCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { opts.Namespace = args[0] if dockerCli.ClientInfo().HasKubernetes() { - kli, err := kubernetes.WrapCli(dockerCli, cmd) + kli, err := kubernetes.WrapCli(dockerCli, kubernetes.NewOptions(cmd.Flags())) if err != nil { return err } diff --git a/components/cli/cli/command/stack/kubernetes/check.go b/components/cli/cli/command/stack/kubernetes/check.go deleted file mode 100644 index 55411b3dd3..0000000000 --- a/components/cli/cli/command/stack/kubernetes/check.go +++ /dev/null @@ -1,32 +0,0 @@ -package kubernetes - -import ( - "fmt" - - apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1" - log "github.com/sirupsen/logrus" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" -) - -// APIPresent checks that an API is installed. -func APIPresent(config *rest.Config) error { - log.Debugf("check API present at %s", config.Host) - clients, err := kubernetes.NewForConfig(config) - if err != nil { - return err - } - - groups, err := clients.Discovery().ServerGroups() - if err != nil { - return err - } - - for _, group := range groups.Groups { - if group.Name == apiv1beta1.SchemeGroupVersion.Group { - return nil - } - } - - return fmt.Errorf("could not find %s api. Install it on your cluster first", apiv1beta1.SchemeGroupVersion.Group) -} diff --git a/components/cli/cli/command/stack/kubernetes/cli.go b/components/cli/cli/command/stack/kubernetes/cli.go index 37f826272f..5962e0eab6 100644 --- a/components/cli/cli/command/stack/kubernetes/cli.go +++ b/components/cli/cli/command/stack/kubernetes/cli.go @@ -1,16 +1,17 @@ package kubernetes import ( - "fmt" "os" "path/filepath" "github.com/docker/cli/cli/command" + "github.com/docker/cli/kubernetes" composev1beta1 "github.com/docker/cli/kubernetes/client/clientset_generated/clientset/typed/compose/v1beta1" "github.com/docker/docker/pkg/homedir" - "github.com/spf13/cobra" + "github.com/pkg/errors" + flag "github.com/spf13/pflag" + kubeclient "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" ) // KubeCli holds kubernetes specifics (client, namespace) with the command.Cli @@ -18,28 +19,38 @@ type KubeCli struct { command.Cli kubeConfig *restclient.Config kubeNamespace string + clientSet *kubeclient.Clientset +} + +// Options contains resolved parameters to initialize kubernetes clients +type Options struct { + Namespace string + Config string +} + +// NewOptions returns an Options initialized with command line flags +func NewOptions(flags *flag.FlagSet) Options { + var opts Options + if namespace, err := flags.GetString("namespace"); err == nil { + opts.Namespace = namespace + } + if kubeConfig, err := flags.GetString("kubeconfig"); err == nil { + opts.Config = kubeConfig + } + return opts } // WrapCli wraps command.Cli with kubernetes specifics -func WrapCli(dockerCli command.Cli, cmd *cobra.Command) (*KubeCli, error) { +func WrapCli(dockerCli command.Cli, opts Options) (*KubeCli, error) { var err error cli := &KubeCli{ Cli: dockerCli, kubeNamespace: "default", } - if cmd.Flags().Changed("namespace") { - cli.kubeNamespace, err = cmd.Flags().GetString("namespace") - if err != nil { - return nil, err - } - } - kubeConfig := "" - if cmd.Flags().Changed("kubeconfig") { - kubeConfig, err = cmd.Flags().GetString("kubeconfig") - if err != nil { - return nil, err - } + if opts.Namespace != "" { + cli.kubeNamespace = opts.Namespace } + kubeConfig := opts.Config if kubeConfig == "" { if config := os.Getenv("KUBECONFIG"); config != "" { kubeConfig = config @@ -47,13 +58,18 @@ func WrapCli(dockerCli command.Cli, cmd *cobra.Command) (*KubeCli, error) { kubeConfig = filepath.Join(homedir.Get(), ".kube/config") } } - - config, err := clientcmd.BuildConfigFromFlags("", kubeConfig) + config, err := kubernetes.NewKubernetesConfig(kubeConfig) if err != nil { - return nil, fmt.Errorf("Failed to load kubernetes configuration file '%s'", kubeConfig) + return nil, err } cli.kubeConfig = config + clientSet, err := kubeclient.NewForConfig(config) + if err != nil { + return nil, err + } + cli.clientSet = clientSet + return cli, nil } @@ -62,15 +78,20 @@ func (c *KubeCli) composeClient() (*Factory, error) { } func (c *KubeCli) stacks() (composev1beta1.StackInterface, error) { - err := APIPresent(c.kubeConfig) + version, err := kubernetes.GetStackAPIVersion(c.clientSet) + if err != nil { return nil, err } - clientSet, err := composev1beta1.NewForConfig(c.kubeConfig) - if err != nil { - return nil, err + switch version { + case kubernetes.StackAPIV1Beta1: + clientSet, err := composev1beta1.NewForConfig(c.kubeConfig) + if err != nil { + return nil, err + } + return clientSet.Stacks(c.kubeNamespace), nil + default: + return nil, errors.Errorf("no supported Stack API version") } - - return clientSet.Stacks(c.kubeNamespace), nil } diff --git a/components/cli/cli/command/stack/list.go b/components/cli/cli/command/stack/list.go index 2cb10c1716..b153a8b2f6 100644 --- a/components/cli/cli/command/stack/list.go +++ b/components/cli/cli/command/stack/list.go @@ -19,7 +19,7 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { if dockerCli.ClientInfo().HasKubernetes() { - kli, err := kubernetes.WrapCli(dockerCli, cmd) + kli, err := kubernetes.WrapCli(dockerCli, kubernetes.NewOptions(cmd.Flags())) if err != nil { return err } diff --git a/components/cli/cli/command/stack/ps.go b/components/cli/cli/command/stack/ps.go index 8e835201a2..7c003bda80 100644 --- a/components/cli/cli/command/stack/ps.go +++ b/components/cli/cli/command/stack/ps.go @@ -20,7 +20,7 @@ func newPsCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { opts.Namespace = args[0] if dockerCli.ClientInfo().HasKubernetes() { - kli, err := kubernetes.WrapCli(dockerCli, cmd) + kli, err := kubernetes.WrapCli(dockerCli, kubernetes.NewOptions(cmd.Flags())) if err != nil { return err } diff --git a/components/cli/cli/command/stack/remove.go b/components/cli/cli/command/stack/remove.go index e4dd913dbd..25df2a5b61 100644 --- a/components/cli/cli/command/stack/remove.go +++ b/components/cli/cli/command/stack/remove.go @@ -20,7 +20,7 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { opts.Namespaces = args if dockerCli.ClientInfo().HasKubernetes() { - kli, err := kubernetes.WrapCli(dockerCli, cmd) + kli, err := kubernetes.WrapCli(dockerCli, kubernetes.NewOptions(cmd.Flags())) if err != nil { return err } diff --git a/components/cli/cli/command/stack/services.go b/components/cli/cli/command/stack/services.go index 7f427c170e..c05d3e51c1 100644 --- a/components/cli/cli/command/stack/services.go +++ b/components/cli/cli/command/stack/services.go @@ -20,7 +20,7 @@ func newServicesCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { opts.Namespace = args[0] if dockerCli.ClientInfo().HasKubernetes() { - kli, err := kubernetes.WrapCli(dockerCli, cmd) + kli, err := kubernetes.WrapCli(dockerCli, kubernetes.NewOptions(cmd.Flags())) if err != nil { return err } diff --git a/components/cli/cli/command/system/version.go b/components/cli/cli/command/system/version.go index 8223b6df80..20995dc38a 100644 --- a/components/cli/cli/command/system/version.go +++ b/components/cli/cli/command/system/version.go @@ -9,10 +9,13 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/kubernetes" "github.com/docker/cli/templates" "github.com/docker/docker/api/types" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" "golang.org/x/net/context" + kubernetesClient "k8s.io/client-go/kubernetes" ) var versionTemplate = `{{with .Client -}} @@ -48,16 +51,23 @@ Server:{{if ne .Platform.Name ""}} {{.Platform.Name}}{{end}} {{- end}} {{- end}} {{- end}} + {{- end}}{{- end}} + {{- if .KubernetesOK}}{{with .Kubernetes}} + Kubernetes: + Version: {{.Kubernetes}} + Stack API: {{.StackAPI}} {{- end}}{{end}}` type versionOptions struct { - format string + format string + kubeConfig string } // versionInfo contains version information of both the Client, and Server type versionInfo struct { - Client clientVersion - Server *types.Version + Client clientVersion + Server *types.Version + Kubernetes *kubernetesVersion } type clientVersion struct { @@ -75,12 +85,21 @@ type clientVersion struct { Orchestrator string `json:",omitempty"` } +type kubernetesVersion struct { + Kubernetes string + StackAPI string +} + // ServerOK returns true when the client could connect to the docker server // and parse the information received. It returns false otherwise. func (v versionInfo) ServerOK() bool { return v.Server != nil } +func (v versionInfo) KubernetesOK() bool { + return v.Kubernetes != nil +} + // NewVersionCommand creates a new cobra.Command for `docker version` func NewVersionCommand(dockerCli command.Cli) *cobra.Command { var opts versionOptions @@ -95,8 +114,10 @@ func NewVersionCommand(dockerCli command.Cli) *cobra.Command { } flags := cmd.Flags() - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") + flags.StringVarP(&opts.kubeConfig, "kubeconfig", "k", "", "Kubernetes config file") + flags.SetAnnotation("kubeconfig", "kubernetes", nil) + flags.SetAnnotation("kubeconfig", "experimentalCLI", nil) return cmd } @@ -139,6 +160,7 @@ func runVersion(dockerCli command.Cli, opts *versionOptions) error { Experimental: dockerCli.ClientInfo().HasExperimental, Orchestrator: string(dockerCli.ClientInfo().Orchestrator), }, + Kubernetes: getKubernetesVersion(dockerCli, opts.kubeConfig), } sv, err := dockerCli.Client().ServerVersion(context.Background()) @@ -189,3 +211,45 @@ func getDetailsOrder(v types.ComponentVersion) []string { sort.Strings(out) return out } + +func getKubernetesVersion(dockerCli command.Cli, kubeConfig string) *kubernetesVersion { + if !dockerCli.ClientInfo().HasKubernetes() { + return nil + } + + version := kubernetesVersion{ + Kubernetes: "Unknown", + StackAPI: "Unknown", + } + config, err := kubernetes.NewKubernetesConfig(kubeConfig) + if err != nil { + logrus.Debugf("failed to get Kubernetes configuration: %s", err) + return &version + } + kubeClient, err := kubernetesClient.NewForConfig(config) + if err != nil { + logrus.Debugf("failed to get Kubernetes client: %s", err) + return &version + } + version.StackAPI = getStackVersion(kubeClient) + version.Kubernetes = getKubernetesServerVersion(kubeClient) + return &version +} + +func getStackVersion(client *kubernetesClient.Clientset) string { + apiVersion, err := kubernetes.GetStackAPIVersion(client) + if err != nil { + logrus.Debugf("failed to get Stack API version: %s", err) + return "Unknown" + } + return string(apiVersion) +} + +func getKubernetesServerVersion(client *kubernetesClient.Clientset) string { + kubeVersion, err := client.DiscoveryClient.ServerVersion() + if err != nil { + logrus.Debugf("failed to get Kubernetes server version: %s", err) + return "Unknown" + } + return kubeVersion.String() +} diff --git a/components/cli/kubernetes/check.go b/components/cli/kubernetes/check.go new file mode 100644 index 0000000000..f8dee3d052 --- /dev/null +++ b/components/cli/kubernetes/check.go @@ -0,0 +1,50 @@ +package kubernetes + +import ( + apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1" + "github.com/pkg/errors" + apimachinerymetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/kubernetes" +) + +// StackVersion represents the detected Compose Component on Kubernetes side. +type StackVersion string + +const ( + // StackAPIV1Beta1 is returned if it's the most recent version available. + StackAPIV1Beta1 = StackVersion("v1beta1") +) + +// GetStackAPIVersion returns the most recent stack API installed. +func GetStackAPIVersion(clientSet *kubernetes.Clientset) (StackVersion, error) { + groups, err := clientSet.Discovery().ServerGroups() + if err != nil { + return "", err + } + + return getAPIVersion(groups) +} + +func getAPIVersion(groups *metav1.APIGroupList) (StackVersion, error) { + switch { + case findVersion(apiv1beta1.SchemeGroupVersion, groups.Groups): + return StackAPIV1Beta1, nil + default: + return "", errors.Errorf("failed to find a Stack API version") + } +} + +func findVersion(stackAPI schema.GroupVersion, groups []apimachinerymetav1.APIGroup) bool { + for _, group := range groups { + if group.Name == stackAPI.Group { + for _, version := range group.Versions { + if version.Version == stackAPI.Version { + return true + } + } + } + } + return false +} diff --git a/components/cli/kubernetes/check_test.go b/components/cli/kubernetes/check_test.go new file mode 100644 index 0000000000..0368e05228 --- /dev/null +++ b/components/cli/kubernetes/check_test.go @@ -0,0 +1,49 @@ +package kubernetes + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestGetStackAPIVersion(t *testing.T) { + var tests = []struct { + description string + groups *metav1.APIGroupList + err bool + expectedStack StackVersion + }{ + {"no stack api", makeGroups(), true, ""}, + {"v1beta1", makeGroups(groupVersion{"compose.docker.com", []string{"v1beta1"}}), false, StackAPIV1Beta1}, + } + + for _, test := range tests { + version, err := getAPIVersion(test.groups) + if test.err { + require.Error(t, err) + } else { + require.NoError(t, err) + } + assert.Equal(t, test.expectedStack, version) + } +} + +type groupVersion struct { + name string + versions []string +} + +func makeGroups(versions ...groupVersion) *metav1.APIGroupList { + groups := make([]metav1.APIGroup, len(versions)) + for i := range versions { + groups[i].Name = versions[i].name + for _, v := range versions[i].versions { + groups[i].Versions = append(groups[i].Versions, metav1.GroupVersionForDiscovery{Version: v}) + } + } + return &metav1.APIGroupList{ + Groups: groups, + } +} diff --git a/components/cli/kubernetes/config.go b/components/cli/kubernetes/config.go new file mode 100644 index 0000000000..3a9fdfc6dc --- /dev/null +++ b/components/cli/kubernetes/config.go @@ -0,0 +1,24 @@ +package kubernetes + +import ( + "os" + "path/filepath" + + "github.com/docker/docker/pkg/homedir" + restclient "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +// NewKubernetesConfig resolves the path to the desired Kubernetes configuration file, depending +// environment variable and command line flag. +func NewKubernetesConfig(configFlag string) (*restclient.Config, error) { + kubeConfig := configFlag + if kubeConfig == "" { + if config := os.Getenv("KUBECONFIG"); config != "" { + kubeConfig = config + } else { + kubeConfig = filepath.Join(homedir.Get(), ".kube/config") + } + } + return clientcmd.BuildConfigFromFlags("", kubeConfig) +} From faa59fb28892015a9fc8faf343c9898447234356 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 26 Feb 2018 15:27:11 +0900 Subject: [PATCH 03/13] docs: mention sctp Signed-off-by: Akihiro Suda Upstream-commit: b85d87b8ab407ac6b6bc694ac87f8ef3244e6eeb Component: cli --- components/cli/docs/reference/commandline/run.md | 8 ++++---- .../cli/docs/reference/commandline/service_create.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/components/cli/docs/reference/commandline/run.md b/components/cli/docs/reference/commandline/run.md index 3188e7ab6f..4ec793a217 100644 --- a/components/cli/docs/reference/commandline/run.md +++ b/components/cli/docs/reference/commandline/run.md @@ -343,12 +343,12 @@ $ docker run -t -i --mount type=bind,src=/data,dst=/data busybox sh ### Publish or expose port (-p, --expose) ```bash -$ docker run -p 127.0.0.1:80:8080 ubuntu bash +$ docker run -p 127.0.0.1:80:8080/tcp ubuntu bash ``` -This binds port `8080` of the container to port `80` on `127.0.0.1` of the host -machine. The [Docker User -Guide](https://docs.docker.com/engine/userguide/networking/default_network/dockerlinks/) +This binds port `8080` of the container to TCP port `80` on `127.0.0.1` of the host +machine. You can also specify `udp` and `sctp` ports. +The [Docker User Guide](https://docs.docker.com/engine/userguide/networking/default_network/dockerlinks/) explains in detail how to manipulate ports in Docker. ```bash diff --git a/components/cli/docs/reference/commandline/service_create.md b/components/cli/docs/reference/commandline/service_create.md index 2eeae5d13e..cdda1373ae 100644 --- a/components/cli/docs/reference/commandline/service_create.md +++ b/components/cli/docs/reference/commandline/service_create.md @@ -813,7 +813,7 @@ The options you can specify are: --publish 8080:80/tcp --publish published=8080,target=80,protocol=tcp

- The protocol to use, either tcp or udp Defaults to + The protocol to use, tcp , udp, or sctp. Defaults to tcp. To bind a port for both protocols, specify the -p or --publish flag twice.

From 9f5c7766d0896bde26b3589f3d2267373897d9d0 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 26 Feb 2018 11:06:07 +0100 Subject: [PATCH 04/13] Bump VERSION to 18.04.0-dev The Docker 18.03 branch was cut from CLI: 84c7dd605770658e0995df2d9690093d440bc1d5 Engine: fb005971c2bd82de482d1cfe924f2f4ec60c60ae Signed-off-by: Sebastiaan van Stijn Upstream-commit: 06dd62ab1eb56a695974277c0e858b62e9abfc43 Component: cli --- components/cli/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/VERSION b/components/cli/VERSION index f3aa77b51e..cc7864ad80 100644 --- a/components/cli/VERSION +++ b/components/cli/VERSION @@ -1 +1 @@ -18.03.0-dev +18.04.0-dev From 99c3d5bb043560ebf6c2816cbc43d798b8d4eb47 Mon Sep 17 00:00:00 2001 From: Arash Deshmeh Date: Wed, 21 Feb 2018 06:29:08 -0500 Subject: [PATCH 05/13] added support for mandatory variables to cli/compose Signed-off-by: Arash Deshmeh Upstream-commit: 99ecf57c6c2ecbe34e108a555c0541d6e2f8405f Component: cli --- .../cli/cli/compose/template/template.go | 22 ++++++- .../cli/cli/compose/template/template_test.go | 60 +++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/components/cli/cli/compose/template/template.go b/components/cli/cli/compose/template/template.go index 8426a23522..9bfc7cec5c 100644 --- a/components/cli/cli/compose/template/template.go +++ b/components/cli/cli/compose/template/template.go @@ -7,7 +7,7 @@ import ( ) var delimiter = "\\$" -var substitution = "[_a-z][_a-z0-9]*(?::?-[^}]+)?" +var substitution = "[_a-z][_a-z0-9]*(?::?[-?][^}]+)?" var patternString = fmt.Sprintf( "%s(?i:(?P%s)|(?P%s)|{(?P%s)}|(?P))", @@ -69,6 +69,26 @@ func Substitute(template string, mapping Mapping) (string, error) { return value } + if strings.Contains(substitution, ":?") { + name, errorMessage := partition(substitution, ":?") + value, ok := mapping(name) + if !ok || value == "" { + err = &InvalidTemplateError{Template: errorMessage} + return "" + } + return value + } + + if strings.Contains(substitution, "?") { + name, errorMessage := partition(substitution, "?") + value, ok := mapping(name) + if !ok { + err = &InvalidTemplateError{Template: errorMessage} + return "" + } + return value + } + // No default (fall back to empty string) value, ok := mapping(substitution) if !ok { diff --git a/components/cli/cli/compose/template/template_test.go b/components/cli/cli/compose/template/template_test.go index 4ad3f252ac..d11b0a8ec3 100644 --- a/components/cli/cli/compose/template/template_test.go +++ b/components/cli/cli/compose/template/template_test.go @@ -81,3 +81,63 @@ func TestNonAlphanumericDefault(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "ok /non:-alphanumeric", result) } + +func TestMandatoryVariableErrors(t *testing.T) { + testCases := []struct { + template string + expectedError string + }{ + { + template: "not ok ${UNSET_VAR:?Mandatory Variable Unset}", + expectedError: "Mandatory Variable Unset", + }, + { + template: "not ok ${BAR:?Mandatory Variable Empty}", + expectedError: "Mandatory Variable Empty", + }, + { + template: "not ok ${UNSET_VAR:?}", + expectedError: "", + }, + { + template: "not ok ${UNSET_VAR?Mandatory Variable Unset", + expectedError: "Mandatory Variable Unset", + }, + { + template: "not ok ${UNSET_VAR?}", + expectedError: "", + }, + } + + for _, tc := range testCases { + _, err := Substitute(tc.template, defaultMapping) + assert.Error(t, err) + assert.IsType(t, &InvalidTemplateError{tc.expectedError}, err) + } +} + +func TestDefaultsForMandatoryVariables(t *testing.T) { + testCases := []struct { + template string + expected string + }{ + { + template: "ok ${FOO:?err}", + expected: "ok first", + }, + { + template: "ok ${FOO?err}", + expected: "ok first", + }, + { + template: "ok ${BAR?err}", + expected: "ok ", + }, + } + + for _, tc := range testCases { + result, err := Substitute(tc.template, defaultMapping) + assert.Nil(t, err) + assert.Equal(t, tc.expected, result) + } +} From 41c5444dac3bf035e98bf9dbcf1aa57867e3f08c Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 21 Feb 2018 15:16:12 -0500 Subject: [PATCH 06/13] Refactor substitute to reduce cyclo complexity Signed-off-by: Daniel Nephin Upstream-commit: ce544823b61b856fad8483e05796a2f2831116ca Component: cli --- .../cli/cli/compose/template/template.go | 95 +++++++++---------- .../cli/cli/compose/template/template_test.go | 7 ++ 2 files changed, 53 insertions(+), 49 deletions(-) diff --git a/components/cli/cli/compose/template/template.go b/components/cli/cli/compose/template/template.go index 9bfc7cec5c..2792a09ba5 100644 --- a/components/cli/cli/compose/template/template.go +++ b/components/cli/cli/compose/template/template.go @@ -37,77 +37,74 @@ func Substitute(template string, mapping Mapping) (string, error) { var err error result := pattern.ReplaceAllStringFunc(template, func(substring string) string { matches := pattern.FindStringSubmatch(substring) - groups := make(map[string]string) - for i, name := range pattern.SubexpNames() { - if i != 0 { - groups[name] = matches[i] - } + groups := matchGroups(matches) + if escaped := groups["escaped"]; escaped != "" { + return escaped } substitution := groups["named"] if substitution == "" { substitution = groups["braced"] } - if substitution != "" { - // Soft default (fall back if unset or empty) - if strings.Contains(substitution, ":-") { - name, defaultValue := partition(substitution, ":-") - value, ok := mapping(name) - if !ok || value == "" { - return defaultValue - } - return value - } - // Hard default (fall back if-and-only-if empty) - if strings.Contains(substitution, "-") { - name, defaultValue := partition(substitution, "-") - value, ok := mapping(name) - if !ok { - return defaultValue - } - return value - } + switch { - if strings.Contains(substitution, ":?") { - name, errorMessage := partition(substitution, ":?") - value, ok := mapping(name) - if !ok || value == "" { - err = &InvalidTemplateError{Template: errorMessage} - return "" - } - return value - } + case substitution == "": + err = &InvalidTemplateError{Template: template} + return "" - if strings.Contains(substitution, "?") { - name, errorMessage := partition(substitution, "?") - value, ok := mapping(name) - if !ok { - err = &InvalidTemplateError{Template: errorMessage} - return "" - } - return value + // Soft default (fall back if unset or empty) + case strings.Contains(substitution, ":-"): + name, defaultValue := partition(substitution, ":-") + value, ok := mapping(name) + if !ok || value == "" { + return defaultValue } + return value - // No default (fall back to empty string) - value, ok := mapping(substitution) + // Hard default (fall back if-and-only-if empty) + case strings.Contains(substitution, "-"): + name, defaultValue := partition(substitution, "-") + value, ok := mapping(name) if !ok { + return defaultValue + } + return value + + case strings.Contains(substitution, ":?"): + name, errorMessage := partition(substitution, ":?") + value, ok := mapping(name) + if !ok || value == "" { + err = &InvalidTemplateError{Template: errorMessage} + return "" + } + return value + + case strings.Contains(substitution, "?"): + name, errorMessage := partition(substitution, "?") + value, ok := mapping(name) + if !ok { + err = &InvalidTemplateError{Template: errorMessage} return "" } return value } - if escaped := groups["escaped"]; escaped != "" { - return escaped - } - - err = &InvalidTemplateError{Template: template} - return "" + value, _ := mapping(substitution) + return value }) return result, err } +func matchGroups(matches []string) map[string]string { + groups := make(map[string]string) + for i, name := range pattern.SubexpNames()[1:] { + groups[name] = matches[i+1] + } + return groups +} + // Split the string at the first occurrence of sep, and return the part before the separator, // and the part after the separator. // diff --git a/components/cli/cli/compose/template/template_test.go b/components/cli/cli/compose/template/template_test.go index d11b0a8ec3..ce75216dde 100644 --- a/components/cli/cli/compose/template/template_test.go +++ b/components/cli/cli/compose/template/template_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var defaults = map[string]string{ @@ -22,6 +23,12 @@ func TestEscaped(t *testing.T) { assert.Equal(t, "${foo}", result) } +func TestSubstituteNoMatch(t *testing.T) { + result, err := Substitute("foo", defaultMapping) + require.NoError(t, err) + require.Equal(t, "foo", result) +} + func TestInvalid(t *testing.T) { invalidTemplates := []string{ "${", From bb11b4e9b1505767ab65c88009d738a153007733 Mon Sep 17 00:00:00 2001 From: Arash Deshmeh Date: Wed, 21 Feb 2018 17:02:38 -0500 Subject: [PATCH 07/13] Added error message check to TestMandatoryVariableErrors test Signed-off-by: Arash Deshmeh Upstream-commit: e33bc48752c35bf3ac4a86e904732aac4527887b Component: cli --- components/cli/cli/compose/template/template_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/cli/cli/compose/template/template_test.go b/components/cli/cli/compose/template/template_test.go index ce75216dde..66e50124ed 100644 --- a/components/cli/cli/compose/template/template_test.go +++ b/components/cli/cli/compose/template/template_test.go @@ -3,6 +3,7 @@ package template import ( "testing" + "github.com/docker/cli/internal/test/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -119,7 +120,8 @@ func TestMandatoryVariableErrors(t *testing.T) { for _, tc := range testCases { _, err := Substitute(tc.template, defaultMapping) assert.Error(t, err) - assert.IsType(t, &InvalidTemplateError{tc.expectedError}, err) + assert.IsType(t, &InvalidTemplateError{}, err) + testutil.ErrorContains(t, err, tc.expectedError) } } From 38e7824f05f4a24a388664ce1ce73ee4b46d49f8 Mon Sep 17 00:00:00 2001 From: Arash Deshmeh Date: Mon, 26 Feb 2018 15:23:52 -0500 Subject: [PATCH 08/13] fix the error message in Substitute function Signed-off-by: Arash Deshmeh Upstream-commit: 5d8ce59a2578d7c145c33461727aed6391aa22bf Component: cli --- components/cli/cli/compose/template/template.go | 10 +++++++--- components/cli/cli/compose/template/template_test.go | 12 ++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/components/cli/cli/compose/template/template.go b/components/cli/cli/compose/template/template.go index 2792a09ba5..086194b8fe 100644 --- a/components/cli/cli/compose/template/template.go +++ b/components/cli/cli/compose/template/template.go @@ -7,7 +7,7 @@ import ( ) var delimiter = "\\$" -var substitution = "[_a-z][_a-z0-9]*(?::?[-?][^}]+)?" +var substitution = "[_a-z][_a-z0-9]*(?::?[-?][^}]*)?" var patternString = fmt.Sprintf( "%s(?i:(?P%s)|(?P%s)|{(?P%s)}|(?P))", @@ -75,7 +75,9 @@ func Substitute(template string, mapping Mapping) (string, error) { name, errorMessage := partition(substitution, ":?") value, ok := mapping(name) if !ok || value == "" { - err = &InvalidTemplateError{Template: errorMessage} + err = &InvalidTemplateError{ + Template: fmt.Sprintf("required variable %s is missing a value: %s", name, errorMessage), + } return "" } return value @@ -84,7 +86,9 @@ func Substitute(template string, mapping Mapping) (string, error) { name, errorMessage := partition(substitution, "?") value, ok := mapping(name) if !ok { - err = &InvalidTemplateError{Template: errorMessage} + err = &InvalidTemplateError{ + Template: fmt.Sprintf("required variable %s is missing a value: %s", name, errorMessage), + } return "" } return value diff --git a/components/cli/cli/compose/template/template_test.go b/components/cli/cli/compose/template/template_test.go index 66e50124ed..4fec57e8e8 100644 --- a/components/cli/cli/compose/template/template_test.go +++ b/components/cli/cli/compose/template/template_test.go @@ -97,23 +97,23 @@ func TestMandatoryVariableErrors(t *testing.T) { }{ { template: "not ok ${UNSET_VAR:?Mandatory Variable Unset}", - expectedError: "Mandatory Variable Unset", + expectedError: "required variable UNSET_VAR is missing a value: Mandatory Variable Unset", }, { template: "not ok ${BAR:?Mandatory Variable Empty}", - expectedError: "Mandatory Variable Empty", + expectedError: "required variable BAR is missing a value: Mandatory Variable Empty", }, { template: "not ok ${UNSET_VAR:?}", - expectedError: "", + expectedError: "required variable UNSET_VAR is missing a value", }, { - template: "not ok ${UNSET_VAR?Mandatory Variable Unset", - expectedError: "Mandatory Variable Unset", + template: "not ok ${UNSET_VAR?Mandatory Variable Unset}", + expectedError: "required variable UNSET_VAR is missing a value: Mandatory Variable Unset", }, { template: "not ok ${UNSET_VAR?}", - expectedError: "", + expectedError: "required variable UNSET_VAR is missing a value", }, } From 70e2992847956500955d46467a65267609dc5442 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Tue, 27 Feb 2018 12:14:07 +0100 Subject: [PATCH 09/13] Fix `--label` behavior on run Commit 2b17f4c8a8caad552025edb05a73db683fb8a5c6 fixed the way empty labels are taken into account (i.e. not interpolated from environment variable), but it created a regression. `ValidateLabel` functions doesn't allow empty label value, but it has always been possible to pass an empty label via the cli (`docker run --label foo`). This fixes that by not validating the label flag. Signed-off-by: Vincent Demeester Upstream-commit: 31dc5c0a9a8bdc11c7ad335aebb753ed527caa5a Component: cli --- .../cli/cli/command/container/client_test.go | 8 ++++++ components/cli/cli/command/container/opts.go | 2 +- .../cli/cli/command/container/run_test.go | 25 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 components/cli/cli/command/container/run_test.go diff --git a/components/cli/cli/command/container/client_test.go b/components/cli/cli/command/container/client_test.go index e0ffd2a66e..ed04920b16 100644 --- a/components/cli/cli/command/container/client_test.go +++ b/components/cli/cli/command/container/client_test.go @@ -16,6 +16,7 @@ type fakeClient struct { execInspectFunc func(execID string) (types.ContainerExecInspect, error) execCreateFunc func(container string, config types.ExecConfig) (types.IDResponse, error) createContainerFunc func(config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error) + containerStartFunc func(container string, options types.ContainerStartOptions) error imageCreateFunc func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) infoFunc func() (types.Info, error) containerStatPathFunc func(container, path string) (types.ContainerPathStat, error) @@ -116,3 +117,10 @@ func (f *fakeClient) ContainerWait(_ context.Context, container string, _ contai } return nil, nil } + +func (f *fakeClient) ContainerStart(_ context.Context, container string, options types.ContainerStartOptions) error { + if f.containerStartFunc != nil { + return f.containerStartFunc(container, options) + } + return nil +} diff --git a/components/cli/cli/command/container/opts.go b/components/cli/cli/command/container/opts.go index 0adecca759..b8ff5e4a4a 100644 --- a/components/cli/cli/command/container/opts.go +++ b/components/cli/cli/command/container/opts.go @@ -145,7 +145,7 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { expose: opts.NewListOpts(nil), extraHosts: opts.NewListOpts(opts.ValidateExtraHost), groupAdd: opts.NewListOpts(nil), - labels: opts.NewListOpts(opts.ValidateLabel), + labels: opts.NewListOpts(nil), labelsFile: opts.NewListOpts(nil), linkLocalIPs: opts.NewListOpts(nil), links: opts.NewListOpts(opts.ValidateLink), diff --git a/components/cli/cli/command/container/run_test.go b/components/cli/cli/command/container/run_test.go new file mode 100644 index 0000000000..ca08fb3362 --- /dev/null +++ b/components/cli/cli/command/container/run_test.go @@ -0,0 +1,25 @@ +package container + +import ( + "testing" + + "github.com/docker/cli/internal/test" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" + "github.com/stretchr/testify/assert" +) + +func TestRunLabel(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + createContainerFunc: func(_ *container.Config, _ *container.HostConfig, _ *network.NetworkingConfig, _ string) (container.ContainerCreateCreatedBody, error) { + return container.ContainerCreateCreatedBody{ + ID: "id", + }, nil + }, + Version: "1.36", + }) + cmd := NewRunCommand(cli) + cmd.Flags().Set("detach", "true") + cmd.SetArgs([]string{"--label", "foo", "busybox"}) + assert.NoError(t, cmd.Execute()) +} From 697a64b33ec964f8cf04c84b3b2f10bad6d17ba4 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 27 Feb 2018 16:38:02 -0500 Subject: [PATCH 10/13] Add more container cp tests Signed-off-by: Daniel Nephin Upstream-commit: 07cb69e9bce40495b489905e6795dc048232b324 Component: cli --- .../cli/cli/command/container/cp_test.go | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/components/cli/cli/command/container/cp_test.go b/components/cli/cli/command/container/cp_test.go index db7760c4b2..ce4430746e 100644 --- a/components/cli/cli/command/container/cp_test.go +++ b/components/cli/cli/command/container/cp_test.go @@ -3,6 +3,7 @@ package container import ( "io" "io/ioutil" + "os" "runtime" "strings" "testing" @@ -111,6 +112,33 @@ func TestRunCopyFromContainerToFilesystemMissingDestinationDirectory(t *testing. testutil.ErrorContains(t, err, destDir.Join("missing")) } +func TestRunCopyToContainerFromFileWithTrailingSlash(t *testing.T) { + srcFile := fs.NewFile(t, t.Name()) + defer srcFile.Remove() + + options := copyOptions{ + source: srcFile.Path() + string(os.PathSeparator), + destination: "container:/path", + } + cli := test.NewFakeCli(&fakeClient{}) + err := runCopy(cli, options) + testutil.ErrorContains(t, err, "not a directory") +} + +func TestRunCopyToContainerSourceDoesNotExist(t *testing.T) { + options := copyOptions{ + source: "/does/not/exist", + destination: "container:/path", + } + cli := test.NewFakeCli(&fakeClient{}) + err := runCopy(cli, options) + expected := "no such file or directory" + if runtime.GOOS == "windows" { + expected = "cannot find the file specified" + } + testutil.ErrorContains(t, err, expected) +} + func TestSplitCpArg(t *testing.T) { var testcases = []struct { doc string From 63f4daf3610e51d145f686594f5af22f15536b96 Mon Sep 17 00:00:00 2001 From: Lydell Manganti Date: Wed, 28 Feb 2018 20:56:19 +1000 Subject: [PATCH 11/13] Update run.md --restart to include unless-stopped Update --restart option to include unless-stopped to be consistent with https://docs.docker.com/config/containers/start-containers-automatically/#use-a-restart-policy Signed-off-by: Lydell Manganti Upstream-commit: d281b72a9890d2085d9c3b700db291039e970765 Component: cli --- components/cli/docs/reference/commandline/run.md | 1 + 1 file changed, 1 insertion(+) diff --git a/components/cli/docs/reference/commandline/run.md b/components/cli/docs/reference/commandline/run.md index 4ec793a217..da5254475e 100644 --- a/components/cli/docs/reference/commandline/run.md +++ b/components/cli/docs/reference/commandline/run.md @@ -592,6 +592,7 @@ Docker supports the following restart policies: |:---------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `no` | Do not automatically restart the container when it exits. This is the default. | | `on-failure[:max-retries]` | Restart only if the container exits with a non-zero exit status. Optionally, limit the number of restart retries the Docker daemon attempts. | +| `unless-stopped` | Restart the container unless it is explicitly stopped or Docker itself is stopped or restarted. | | `always` | Always restart the container regardless of the exit status. When you specify always, the Docker daemon will try to restart the container indefinitely. The container will also always start on daemon startup, regardless of the current state of the container. | ```bash From 919ca49c3003815d3f2732099811764d4e405ee0 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Wed, 28 Feb 2018 16:11:02 +0100 Subject: [PATCH 12/13] Update gotestyourself dependency Signed-off-by: Vincent Demeester Upstream-commit: 98ba439f675cf4ed9fd37d86c3a74beaf065f40a Component: cli --- components/cli/vendor.conf | 3 +- .../vendor/github.com/google/go-cmp/LICENSE | 27 + .../vendor/github.com/google/go-cmp/README.md | 44 ++ .../github.com/google/go-cmp/cmp/compare.go | 553 ++++++++++++++++++ .../go-cmp/cmp/internal/diff/debug_disable.go | 17 + .../go-cmp/cmp/internal/diff/debug_enable.go | 122 ++++ .../google/go-cmp/cmp/internal/diff/diff.go | 363 ++++++++++++ .../go-cmp/cmp/internal/function/func.go | 49 ++ .../go-cmp/cmp/internal/value/format.go | 277 +++++++++ .../google/go-cmp/cmp/internal/value/sort.go | 111 ++++ .../github.com/google/go-cmp/cmp/options.go | 453 ++++++++++++++ .../github.com/google/go-cmp/cmp/path.go | 309 ++++++++++ .../github.com/google/go-cmp/cmp/reporter.go | 53 ++ .../google/go-cmp/cmp/unsafe_panic.go | 15 + .../google/go-cmp/cmp/unsafe_reflect.go | 23 + .../gotestyourself/gotestyourself/README.md | 16 +- .../gotestyourself/assert/assert.go | 250 ++++++++ .../gotestyourself/assert/cmp/compare.go | 310 ++++++++++ .../gotestyourself/assert/cmp/result.go | 94 +++ .../gotestyourself/assert/result.go | 91 +++ .../gotestyourself/gotestyourself/env/env.go | 52 +- .../gotestyourself/gotestyourself/fs/file.go | 32 +- .../gotestyourself/gotestyourself/fs/ops.go | 31 + .../gotestyourself/golden/golden.go | 125 +++- .../gotestyourself/icmd/command.go | 47 +- .../gotestyourself/internal/format/format.go | 27 + .../gotestyourself/internal/source/source.go | 163 ++++++ .../gotestyourself/poll/poll.go | 7 + .../gotestyourself/skip/skip.go | 133 ++--- .../gotestyourself/skip/skip_go18.go | 17 - .../gotestyourself/skip/skip_go19.go | 17 - 31 files changed, 3630 insertions(+), 201 deletions(-) create mode 100644 components/cli/vendor/github.com/google/go-cmp/LICENSE create mode 100644 components/cli/vendor/github.com/google/go-cmp/README.md create mode 100644 components/cli/vendor/github.com/google/go-cmp/cmp/compare.go create mode 100644 components/cli/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go create mode 100644 components/cli/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go create mode 100644 components/cli/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go create mode 100644 components/cli/vendor/github.com/google/go-cmp/cmp/internal/function/func.go create mode 100644 components/cli/vendor/github.com/google/go-cmp/cmp/internal/value/format.go create mode 100644 components/cli/vendor/github.com/google/go-cmp/cmp/internal/value/sort.go create mode 100644 components/cli/vendor/github.com/google/go-cmp/cmp/options.go create mode 100644 components/cli/vendor/github.com/google/go-cmp/cmp/path.go create mode 100644 components/cli/vendor/github.com/google/go-cmp/cmp/reporter.go create mode 100644 components/cli/vendor/github.com/google/go-cmp/cmp/unsafe_panic.go create mode 100644 components/cli/vendor/github.com/google/go-cmp/cmp/unsafe_reflect.go create mode 100644 components/cli/vendor/github.com/gotestyourself/gotestyourself/assert/assert.go create mode 100644 components/cli/vendor/github.com/gotestyourself/gotestyourself/assert/cmp/compare.go create mode 100644 components/cli/vendor/github.com/gotestyourself/gotestyourself/assert/cmp/result.go create mode 100644 components/cli/vendor/github.com/gotestyourself/gotestyourself/assert/result.go create mode 100644 components/cli/vendor/github.com/gotestyourself/gotestyourself/internal/format/format.go create mode 100644 components/cli/vendor/github.com/gotestyourself/gotestyourself/internal/source/source.go delete mode 100644 components/cli/vendor/github.com/gotestyourself/gotestyourself/skip/skip_go18.go delete mode 100644 components/cli/vendor/github.com/gotestyourself/gotestyourself/skip/skip_go19.go diff --git a/components/cli/vendor.conf b/components/cli/vendor.conf index d0ac18a054..cc33be3df5 100755 --- a/components/cli/vendor.conf +++ b/components/cli/vendor.conf @@ -20,13 +20,14 @@ github.com/flynn-archive/go-shlex 3f9db97f856818214da2e1057f8ad84803971cff github.com/ghodss/yaml 0ca9ea5df5451ffdf184b4428c902747c2c11cd7 github.com/gogo/protobuf v0.4 github.com/golang/glog 44145f04b68cf362d9c4df2182967c2275eaefed +github.com/google/go-cmp v0.2.0 github.com/golang/protobuf 7a211bcf3bce0e3f1d74f9894916e6f116ae83b4 github.com/google/btree 316fb6d3f031ae8f4d457c6c5186b9e3ded70435 github.com/google/gofuzz 44d81051d367757e1c7c6a5a86423ece9afcf63c github.com/googleapis/gnostic e4f56557df6250e1945ee6854f181ce4e1c2c646 github.com/gorilla/context v1.1 github.com/gorilla/mux v1.1 -github.com/gotestyourself/gotestyourself v1.2.0 +github.com/gotestyourself/gotestyourself v1.3.0 # FIXME(vdemeester) try to deduplicate this with gojsonpointer github.com/go-openapi/jsonpointer 46af16f9f7b149af66e5d1bd010e3574dc06de98 # FIXME(vdemeester) try to deduplicate this with gojsonreference diff --git a/components/cli/vendor/github.com/google/go-cmp/LICENSE b/components/cli/vendor/github.com/google/go-cmp/LICENSE new file mode 100644 index 0000000000..32017f8fa1 --- /dev/null +++ b/components/cli/vendor/github.com/google/go-cmp/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2017 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/components/cli/vendor/github.com/google/go-cmp/README.md b/components/cli/vendor/github.com/google/go-cmp/README.md new file mode 100644 index 0000000000..61c9c4cd98 --- /dev/null +++ b/components/cli/vendor/github.com/google/go-cmp/README.md @@ -0,0 +1,44 @@ +# Package for equality of Go values + +[![GoDoc](https://godoc.org/github.com/google/go-cmp/cmp?status.svg)][godoc] +[![Build Status](https://travis-ci.org/google/go-cmp.svg?branch=master)][travis] + +This package is intended to be a more powerful and safer alternative to +`reflect.DeepEqual` for comparing whether two values are semantically equal. + +The primary features of `cmp` are: + +* When the default behavior of equality does not suit the needs of the test, + custom equality functions can override the equality operation. + For example, an equality function may report floats as equal so long as they + are within some tolerance of each other. + +* Types that have an `Equal` method may use that method to determine equality. + This allows package authors to determine the equality operation for the types + that they define. + +* If no custom equality functions are used and no `Equal` method is defined, + equality is determined by recursively comparing the primitive kinds on both + values, much like `reflect.DeepEqual`. Unlike `reflect.DeepEqual`, unexported + fields are not compared by default; they result in panics unless suppressed + by using an `Ignore` option (see `cmpopts.IgnoreUnexported`) or explicitly + compared using the `AllowUnexported` option. + +See the [GoDoc documentation][godoc] for more information. + +This is not an official Google product. + +[godoc]: https://godoc.org/github.com/google/go-cmp/cmp +[travis]: https://travis-ci.org/google/go-cmp + +## Install + +``` +go get -u github.com/google/go-cmp/cmp +``` + +## License + +BSD - See [LICENSE][license] file + +[license]: https://github.com/google/go-cmp/blob/master/LICENSE diff --git a/components/cli/vendor/github.com/google/go-cmp/cmp/compare.go b/components/cli/vendor/github.com/google/go-cmp/cmp/compare.go new file mode 100644 index 0000000000..7e215f2202 --- /dev/null +++ b/components/cli/vendor/github.com/google/go-cmp/cmp/compare.go @@ -0,0 +1,553 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// Package cmp determines equality of values. +// +// This package is intended to be a more powerful and safer alternative to +// reflect.DeepEqual for comparing whether two values are semantically equal. +// +// The primary features of cmp are: +// +// • When the default behavior of equality does not suit the needs of the test, +// custom equality functions can override the equality operation. +// For example, an equality function may report floats as equal so long as they +// are within some tolerance of each other. +// +// • Types that have an Equal method may use that method to determine equality. +// This allows package authors to determine the equality operation for the types +// that they define. +// +// • If no custom equality functions are used and no Equal method is defined, +// equality is determined by recursively comparing the primitive kinds on both +// values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, unexported +// fields are not compared by default; they result in panics unless suppressed +// by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly compared +// using the AllowUnexported option. +package cmp + +import ( + "fmt" + "reflect" + + "github.com/google/go-cmp/cmp/internal/diff" + "github.com/google/go-cmp/cmp/internal/function" + "github.com/google/go-cmp/cmp/internal/value" +) + +// BUG(dsnet): Maps with keys containing NaN values cannot be properly compared due to +// the reflection package's inability to retrieve such entries. Equal will panic +// anytime it comes across a NaN key, but this behavior may change. +// +// See https://golang.org/issue/11104 for more details. + +var nothing = reflect.Value{} + +// Equal reports whether x and y are equal by recursively applying the +// following rules in the given order to x and y and all of their sub-values: +// +// • If two values are not of the same type, then they are never equal +// and the overall result is false. +// +// • Let S be the set of all Ignore, Transformer, and Comparer options that +// remain after applying all path filters, value filters, and type filters. +// If at least one Ignore exists in S, then the comparison is ignored. +// If the number of Transformer and Comparer options in S is greater than one, +// then Equal panics because it is ambiguous which option to use. +// If S contains a single Transformer, then use that to transform the current +// values and recursively call Equal on the output values. +// If S contains a single Comparer, then use that to compare the current values. +// Otherwise, evaluation proceeds to the next rule. +// +// • If the values have an Equal method of the form "(T) Equal(T) bool" or +// "(T) Equal(I) bool" where T is assignable to I, then use the result of +// x.Equal(y) even if x or y is nil. +// Otherwise, no such method exists and evaluation proceeds to the next rule. +// +// • Lastly, try to compare x and y based on their basic kinds. +// Simple kinds like booleans, integers, floats, complex numbers, strings, and +// channels are compared using the equivalent of the == operator in Go. +// Functions are only equal if they are both nil, otherwise they are unequal. +// Pointers are equal if the underlying values they point to are also equal. +// Interfaces are equal if their underlying concrete values are also equal. +// +// Structs are equal if all of their fields are equal. If a struct contains +// unexported fields, Equal panics unless the AllowUnexported option is used or +// an Ignore option (e.g., cmpopts.IgnoreUnexported) ignores that field. +// +// Arrays, slices, and maps are equal if they are both nil or both non-nil +// with the same length and the elements at each index or key are equal. +// Note that a non-nil empty slice and a nil slice are not equal. +// To equate empty slices and maps, consider using cmpopts.EquateEmpty. +// Map keys are equal according to the == operator. +// To use custom comparisons for map keys, consider using cmpopts.SortMaps. +func Equal(x, y interface{}, opts ...Option) bool { + s := newState(opts) + s.compareAny(reflect.ValueOf(x), reflect.ValueOf(y)) + return s.result.Equal() +} + +// Diff returns a human-readable report of the differences between two values. +// It returns an empty string if and only if Equal returns true for the same +// input values and options. The output string will use the "-" symbol to +// indicate elements removed from x, and the "+" symbol to indicate elements +// added to y. +// +// Do not depend on this output being stable. +func Diff(x, y interface{}, opts ...Option) string { + r := new(defaultReporter) + opts = Options{Options(opts), r} + eq := Equal(x, y, opts...) + d := r.String() + if (d == "") != eq { + panic("inconsistent difference and equality results") + } + return d +} + +type state struct { + // These fields represent the "comparison state". + // Calling statelessCompare must not result in observable changes to these. + result diff.Result // The current result of comparison + curPath Path // The current path in the value tree + reporter reporter // Optional reporter used for difference formatting + + // dynChecker triggers pseudo-random checks for option correctness. + // It is safe for statelessCompare to mutate this value. + dynChecker dynChecker + + // These fields, once set by processOption, will not change. + exporters map[reflect.Type]bool // Set of structs with unexported field visibility + opts Options // List of all fundamental and filter options +} + +func newState(opts []Option) *state { + s := new(state) + for _, opt := range opts { + s.processOption(opt) + } + return s +} + +func (s *state) processOption(opt Option) { + switch opt := opt.(type) { + case nil: + case Options: + for _, o := range opt { + s.processOption(o) + } + case coreOption: + type filtered interface { + isFiltered() bool + } + if fopt, ok := opt.(filtered); ok && !fopt.isFiltered() { + panic(fmt.Sprintf("cannot use an unfiltered option: %v", opt)) + } + s.opts = append(s.opts, opt) + case visibleStructs: + if s.exporters == nil { + s.exporters = make(map[reflect.Type]bool) + } + for t := range opt { + s.exporters[t] = true + } + case reporter: + if s.reporter != nil { + panic("difference reporter already registered") + } + s.reporter = opt + default: + panic(fmt.Sprintf("unknown option %T", opt)) + } +} + +// statelessCompare compares two values and returns the result. +// This function is stateless in that it does not alter the current result, +// or output to any registered reporters. +func (s *state) statelessCompare(vx, vy reflect.Value) diff.Result { + // We do not save and restore the curPath because all of the compareX + // methods should properly push and pop from the path. + // It is an implementation bug if the contents of curPath differs from + // when calling this function to when returning from it. + + oldResult, oldReporter := s.result, s.reporter + s.result = diff.Result{} // Reset result + s.reporter = nil // Remove reporter to avoid spurious printouts + s.compareAny(vx, vy) + res := s.result + s.result, s.reporter = oldResult, oldReporter + return res +} + +func (s *state) compareAny(vx, vy reflect.Value) { + // TODO: Support cyclic data structures. + + // Rule 0: Differing types are never equal. + if !vx.IsValid() || !vy.IsValid() { + s.report(vx.IsValid() == vy.IsValid(), vx, vy) + return + } + if vx.Type() != vy.Type() { + s.report(false, vx, vy) // Possible for path to be empty + return + } + t := vx.Type() + if len(s.curPath) == 0 { + s.curPath.push(&pathStep{typ: t}) + defer s.curPath.pop() + } + vx, vy = s.tryExporting(vx, vy) + + // Rule 1: Check whether an option applies on this node in the value tree. + if s.tryOptions(vx, vy, t) { + return + } + + // Rule 2: Check whether the type has a valid Equal method. + if s.tryMethod(vx, vy, t) { + return + } + + // Rule 3: Recursively descend into each value's underlying kind. + switch t.Kind() { + case reflect.Bool: + s.report(vx.Bool() == vy.Bool(), vx, vy) + return + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + s.report(vx.Int() == vy.Int(), vx, vy) + return + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + s.report(vx.Uint() == vy.Uint(), vx, vy) + return + case reflect.Float32, reflect.Float64: + s.report(vx.Float() == vy.Float(), vx, vy) + return + case reflect.Complex64, reflect.Complex128: + s.report(vx.Complex() == vy.Complex(), vx, vy) + return + case reflect.String: + s.report(vx.String() == vy.String(), vx, vy) + return + case reflect.Chan, reflect.UnsafePointer: + s.report(vx.Pointer() == vy.Pointer(), vx, vy) + return + case reflect.Func: + s.report(vx.IsNil() && vy.IsNil(), vx, vy) + return + case reflect.Ptr: + if vx.IsNil() || vy.IsNil() { + s.report(vx.IsNil() && vy.IsNil(), vx, vy) + return + } + s.curPath.push(&indirect{pathStep{t.Elem()}}) + defer s.curPath.pop() + s.compareAny(vx.Elem(), vy.Elem()) + return + case reflect.Interface: + if vx.IsNil() || vy.IsNil() { + s.report(vx.IsNil() && vy.IsNil(), vx, vy) + return + } + if vx.Elem().Type() != vy.Elem().Type() { + s.report(false, vx.Elem(), vy.Elem()) + return + } + s.curPath.push(&typeAssertion{pathStep{vx.Elem().Type()}}) + defer s.curPath.pop() + s.compareAny(vx.Elem(), vy.Elem()) + return + case reflect.Slice: + if vx.IsNil() || vy.IsNil() { + s.report(vx.IsNil() && vy.IsNil(), vx, vy) + return + } + fallthrough + case reflect.Array: + s.compareArray(vx, vy, t) + return + case reflect.Map: + s.compareMap(vx, vy, t) + return + case reflect.Struct: + s.compareStruct(vx, vy, t) + return + default: + panic(fmt.Sprintf("%v kind not handled", t.Kind())) + } +} + +func (s *state) tryExporting(vx, vy reflect.Value) (reflect.Value, reflect.Value) { + if sf, ok := s.curPath[len(s.curPath)-1].(*structField); ok && sf.unexported { + if sf.force { + // Use unsafe pointer arithmetic to get read-write access to an + // unexported field in the struct. + vx = unsafeRetrieveField(sf.pvx, sf.field) + vy = unsafeRetrieveField(sf.pvy, sf.field) + } else { + // We are not allowed to export the value, so invalidate them + // so that tryOptions can panic later if not explicitly ignored. + vx = nothing + vy = nothing + } + } + return vx, vy +} + +func (s *state) tryOptions(vx, vy reflect.Value, t reflect.Type) bool { + // If there were no FilterValues, we will not detect invalid inputs, + // so manually check for them and append invalid if necessary. + // We still evaluate the options since an ignore can override invalid. + opts := s.opts + if !vx.IsValid() || !vy.IsValid() { + opts = Options{opts, invalid{}} + } + + // Evaluate all filters and apply the remaining options. + if opt := opts.filter(s, vx, vy, t); opt != nil { + opt.apply(s, vx, vy) + return true + } + return false +} + +func (s *state) tryMethod(vx, vy reflect.Value, t reflect.Type) bool { + // Check if this type even has an Equal method. + m, ok := t.MethodByName("Equal") + if !ok || !function.IsType(m.Type, function.EqualAssignable) { + return false + } + + eq := s.callTTBFunc(m.Func, vx, vy) + s.report(eq, vx, vy) + return true +} + +func (s *state) callTRFunc(f, v reflect.Value) reflect.Value { + v = sanitizeValue(v, f.Type().In(0)) + if !s.dynChecker.Next() { + return f.Call([]reflect.Value{v})[0] + } + + // Run the function twice and ensure that we get the same results back. + // We run in goroutines so that the race detector (if enabled) can detect + // unsafe mutations to the input. + c := make(chan reflect.Value) + go detectRaces(c, f, v) + want := f.Call([]reflect.Value{v})[0] + if got := <-c; !s.statelessCompare(got, want).Equal() { + // To avoid false-positives with non-reflexive equality operations, + // we sanity check whether a value is equal to itself. + if !s.statelessCompare(want, want).Equal() { + return want + } + fn := getFuncName(f.Pointer()) + panic(fmt.Sprintf("non-deterministic function detected: %s", fn)) + } + return want +} + +func (s *state) callTTBFunc(f, x, y reflect.Value) bool { + x = sanitizeValue(x, f.Type().In(0)) + y = sanitizeValue(y, f.Type().In(1)) + if !s.dynChecker.Next() { + return f.Call([]reflect.Value{x, y})[0].Bool() + } + + // Swapping the input arguments is sufficient to check that + // f is symmetric and deterministic. + // We run in goroutines so that the race detector (if enabled) can detect + // unsafe mutations to the input. + c := make(chan reflect.Value) + go detectRaces(c, f, y, x) + want := f.Call([]reflect.Value{x, y})[0].Bool() + if got := <-c; !got.IsValid() || got.Bool() != want { + fn := getFuncName(f.Pointer()) + panic(fmt.Sprintf("non-deterministic or non-symmetric function detected: %s", fn)) + } + return want +} + +func detectRaces(c chan<- reflect.Value, f reflect.Value, vs ...reflect.Value) { + var ret reflect.Value + defer func() { + recover() // Ignore panics, let the other call to f panic instead + c <- ret + }() + ret = f.Call(vs)[0] +} + +// sanitizeValue converts nil interfaces of type T to those of type R, +// assuming that T is assignable to R. +// Otherwise, it returns the input value as is. +func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value { + // TODO(dsnet): Remove this hacky workaround. + // See https://golang.org/issue/22143 + if v.Kind() == reflect.Interface && v.IsNil() && v.Type() != t { + return reflect.New(t).Elem() + } + return v +} + +func (s *state) compareArray(vx, vy reflect.Value, t reflect.Type) { + step := &sliceIndex{pathStep{t.Elem()}, 0, 0} + s.curPath.push(step) + + // Compute an edit-script for slices vx and vy. + es := diff.Difference(vx.Len(), vy.Len(), func(ix, iy int) diff.Result { + step.xkey, step.ykey = ix, iy + return s.statelessCompare(vx.Index(ix), vy.Index(iy)) + }) + + // Report the entire slice as is if the arrays are of primitive kind, + // and the arrays are different enough. + isPrimitive := false + switch t.Elem().Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, + reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: + isPrimitive = true + } + if isPrimitive && es.Dist() > (vx.Len()+vy.Len())/4 { + s.curPath.pop() // Pop first since we are reporting the whole slice + s.report(false, vx, vy) + return + } + + // Replay the edit-script. + var ix, iy int + for _, e := range es { + switch e { + case diff.UniqueX: + step.xkey, step.ykey = ix, -1 + s.report(false, vx.Index(ix), nothing) + ix++ + case diff.UniqueY: + step.xkey, step.ykey = -1, iy + s.report(false, nothing, vy.Index(iy)) + iy++ + default: + step.xkey, step.ykey = ix, iy + if e == diff.Identity { + s.report(true, vx.Index(ix), vy.Index(iy)) + } else { + s.compareAny(vx.Index(ix), vy.Index(iy)) + } + ix++ + iy++ + } + } + s.curPath.pop() + return +} + +func (s *state) compareMap(vx, vy reflect.Value, t reflect.Type) { + if vx.IsNil() || vy.IsNil() { + s.report(vx.IsNil() && vy.IsNil(), vx, vy) + return + } + + // We combine and sort the two map keys so that we can perform the + // comparisons in a deterministic order. + step := &mapIndex{pathStep: pathStep{t.Elem()}} + s.curPath.push(step) + defer s.curPath.pop() + for _, k := range value.SortKeys(append(vx.MapKeys(), vy.MapKeys()...)) { + step.key = k + vvx := vx.MapIndex(k) + vvy := vy.MapIndex(k) + switch { + case vvx.IsValid() && vvy.IsValid(): + s.compareAny(vvx, vvy) + case vvx.IsValid() && !vvy.IsValid(): + s.report(false, vvx, nothing) + case !vvx.IsValid() && vvy.IsValid(): + s.report(false, nothing, vvy) + default: + // It is possible for both vvx and vvy to be invalid if the + // key contained a NaN value in it. There is no way in + // reflection to be able to retrieve these values. + // See https://golang.org/issue/11104 + panic(fmt.Sprintf("%#v has map key with NaNs", s.curPath)) + } + } +} + +func (s *state) compareStruct(vx, vy reflect.Value, t reflect.Type) { + var vax, vay reflect.Value // Addressable versions of vx and vy + + step := &structField{} + s.curPath.push(step) + defer s.curPath.pop() + for i := 0; i < t.NumField(); i++ { + vvx := vx.Field(i) + vvy := vy.Field(i) + step.typ = t.Field(i).Type + step.name = t.Field(i).Name + step.idx = i + step.unexported = !isExported(step.name) + if step.unexported { + // Defer checking of unexported fields until later to give an + // Ignore a chance to ignore the field. + if !vax.IsValid() || !vay.IsValid() { + // For unsafeRetrieveField to work, the parent struct must + // be addressable. Create a new copy of the values if + // necessary to make them addressable. + vax = makeAddressable(vx) + vay = makeAddressable(vy) + } + step.force = s.exporters[t] + step.pvx = vax + step.pvy = vay + step.field = t.Field(i) + } + s.compareAny(vvx, vvy) + } +} + +// report records the result of a single comparison. +// It also calls Report if any reporter is registered. +func (s *state) report(eq bool, vx, vy reflect.Value) { + if eq { + s.result.NSame++ + } else { + s.result.NDiff++ + } + if s.reporter != nil { + s.reporter.Report(vx, vy, eq, s.curPath) + } +} + +// dynChecker tracks the state needed to periodically perform checks that +// user provided functions are symmetric and deterministic. +// The zero value is safe for immediate use. +type dynChecker struct{ curr, next int } + +// Next increments the state and reports whether a check should be performed. +// +// Checks occur every Nth function call, where N is a triangular number: +// 0 1 3 6 10 15 21 28 36 45 55 66 78 91 105 120 136 153 171 190 ... +// See https://en.wikipedia.org/wiki/Triangular_number +// +// This sequence ensures that the cost of checks drops significantly as +// the number of functions calls grows larger. +func (dc *dynChecker) Next() bool { + ok := dc.curr == dc.next + if ok { + dc.curr = 0 + dc.next++ + } + dc.curr++ + return ok +} + +// makeAddressable returns a value that is always addressable. +// It returns the input verbatim if it is already addressable, +// otherwise it creates a new value and returns an addressable copy. +func makeAddressable(v reflect.Value) reflect.Value { + if v.CanAddr() { + return v + } + vc := reflect.New(v.Type()).Elem() + vc.Set(v) + return vc +} diff --git a/components/cli/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go b/components/cli/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go new file mode 100644 index 0000000000..42afa4960e --- /dev/null +++ b/components/cli/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go @@ -0,0 +1,17 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// +build !debug + +package diff + +var debug debugger + +type debugger struct{} + +func (debugger) Begin(_, _ int, f EqualFunc, _, _ *EditScript) EqualFunc { + return f +} +func (debugger) Update() {} +func (debugger) Finish() {} diff --git a/components/cli/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go b/components/cli/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go new file mode 100644 index 0000000000..fd9f7f1773 --- /dev/null +++ b/components/cli/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go @@ -0,0 +1,122 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// +build debug + +package diff + +import ( + "fmt" + "strings" + "sync" + "time" +) + +// The algorithm can be seen running in real-time by enabling debugging: +// go test -tags=debug -v +// +// Example output: +// === RUN TestDifference/#34 +// ┌───────────────────────────────┐ +// │ \ · · · · · · · · · · · · · · │ +// │ · # · · · · · · · · · · · · · │ +// │ · \ · · · · · · · · · · · · · │ +// │ · · \ · · · · · · · · · · · · │ +// │ · · · X # · · · · · · · · · · │ +// │ · · · # \ · · · · · · · · · · │ +// │ · · · · · # # · · · · · · · · │ +// │ · · · · · # \ · · · · · · · · │ +// │ · · · · · · · \ · · · · · · · │ +// │ · · · · · · · · \ · · · · · · │ +// │ · · · · · · · · · \ · · · · · │ +// │ · · · · · · · · · · \ · · # · │ +// │ · · · · · · · · · · · \ # # · │ +// │ · · · · · · · · · · · # # # · │ +// │ · · · · · · · · · · # # # # · │ +// │ · · · · · · · · · # # # # # · │ +// │ · · · · · · · · · · · · · · \ │ +// └───────────────────────────────┘ +// [.Y..M.XY......YXYXY.|] +// +// The grid represents the edit-graph where the horizontal axis represents +// list X and the vertical axis represents list Y. The start of the two lists +// is the top-left, while the ends are the bottom-right. The '·' represents +// an unexplored node in the graph. The '\' indicates that the two symbols +// from list X and Y are equal. The 'X' indicates that two symbols are similar +// (but not exactly equal) to each other. The '#' indicates that the two symbols +// are different (and not similar). The algorithm traverses this graph trying to +// make the paths starting in the top-left and the bottom-right connect. +// +// The series of '.', 'X', 'Y', and 'M' characters at the bottom represents +// the currently established path from the forward and reverse searches, +// separated by a '|' character. + +const ( + updateDelay = 100 * time.Millisecond + finishDelay = 500 * time.Millisecond + ansiTerminal = true // ANSI escape codes used to move terminal cursor +) + +var debug debugger + +type debugger struct { + sync.Mutex + p1, p2 EditScript + fwdPath, revPath *EditScript + grid []byte + lines int +} + +func (dbg *debugger) Begin(nx, ny int, f EqualFunc, p1, p2 *EditScript) EqualFunc { + dbg.Lock() + dbg.fwdPath, dbg.revPath = p1, p2 + top := "┌─" + strings.Repeat("──", nx) + "┐\n" + row := "│ " + strings.Repeat("· ", nx) + "│\n" + btm := "└─" + strings.Repeat("──", nx) + "┘\n" + dbg.grid = []byte(top + strings.Repeat(row, ny) + btm) + dbg.lines = strings.Count(dbg.String(), "\n") + fmt.Print(dbg) + + // Wrap the EqualFunc so that we can intercept each result. + return func(ix, iy int) (r Result) { + cell := dbg.grid[len(top)+iy*len(row):][len("│ ")+len("· ")*ix:][:len("·")] + for i := range cell { + cell[i] = 0 // Zero out the multiple bytes of UTF-8 middle-dot + } + switch r = f(ix, iy); { + case r.Equal(): + cell[0] = '\\' + case r.Similar(): + cell[0] = 'X' + default: + cell[0] = '#' + } + return + } +} + +func (dbg *debugger) Update() { + dbg.print(updateDelay) +} + +func (dbg *debugger) Finish() { + dbg.print(finishDelay) + dbg.Unlock() +} + +func (dbg *debugger) String() string { + dbg.p1, dbg.p2 = *dbg.fwdPath, dbg.p2[:0] + for i := len(*dbg.revPath) - 1; i >= 0; i-- { + dbg.p2 = append(dbg.p2, (*dbg.revPath)[i]) + } + return fmt.Sprintf("%s[%v|%v]\n\n", dbg.grid, dbg.p1, dbg.p2) +} + +func (dbg *debugger) print(d time.Duration) { + if ansiTerminal { + fmt.Printf("\x1b[%dA", dbg.lines) // Reset terminal cursor + } + fmt.Print(dbg) + time.Sleep(d) +} diff --git a/components/cli/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go b/components/cli/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go new file mode 100644 index 0000000000..260befea2f --- /dev/null +++ b/components/cli/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go @@ -0,0 +1,363 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// Package diff implements an algorithm for producing edit-scripts. +// The edit-script is a sequence of operations needed to transform one list +// of symbols into another (or vice-versa). The edits allowed are insertions, +// deletions, and modifications. The summation of all edits is called the +// Levenshtein distance as this problem is well-known in computer science. +// +// This package prioritizes performance over accuracy. That is, the run time +// is more important than obtaining a minimal Levenshtein distance. +package diff + +// EditType represents a single operation within an edit-script. +type EditType uint8 + +const ( + // Identity indicates that a symbol pair is identical in both list X and Y. + Identity EditType = iota + // UniqueX indicates that a symbol only exists in X and not Y. + UniqueX + // UniqueY indicates that a symbol only exists in Y and not X. + UniqueY + // Modified indicates that a symbol pair is a modification of each other. + Modified +) + +// EditScript represents the series of differences between two lists. +type EditScript []EditType + +// String returns a human-readable string representing the edit-script where +// Identity, UniqueX, UniqueY, and Modified are represented by the +// '.', 'X', 'Y', and 'M' characters, respectively. +func (es EditScript) String() string { + b := make([]byte, len(es)) + for i, e := range es { + switch e { + case Identity: + b[i] = '.' + case UniqueX: + b[i] = 'X' + case UniqueY: + b[i] = 'Y' + case Modified: + b[i] = 'M' + default: + panic("invalid edit-type") + } + } + return string(b) +} + +// stats returns a histogram of the number of each type of edit operation. +func (es EditScript) stats() (s struct{ NI, NX, NY, NM int }) { + for _, e := range es { + switch e { + case Identity: + s.NI++ + case UniqueX: + s.NX++ + case UniqueY: + s.NY++ + case Modified: + s.NM++ + default: + panic("invalid edit-type") + } + } + return +} + +// Dist is the Levenshtein distance and is guaranteed to be 0 if and only if +// lists X and Y are equal. +func (es EditScript) Dist() int { return len(es) - es.stats().NI } + +// LenX is the length of the X list. +func (es EditScript) LenX() int { return len(es) - es.stats().NY } + +// LenY is the length of the Y list. +func (es EditScript) LenY() int { return len(es) - es.stats().NX } + +// EqualFunc reports whether the symbols at indexes ix and iy are equal. +// When called by Difference, the index is guaranteed to be within nx and ny. +type EqualFunc func(ix int, iy int) Result + +// Result is the result of comparison. +// NSame is the number of sub-elements that are equal. +// NDiff is the number of sub-elements that are not equal. +type Result struct{ NSame, NDiff int } + +// Equal indicates whether the symbols are equal. Two symbols are equal +// if and only if NDiff == 0. If Equal, then they are also Similar. +func (r Result) Equal() bool { return r.NDiff == 0 } + +// Similar indicates whether two symbols are similar and may be represented +// by using the Modified type. As a special case, we consider binary comparisons +// (i.e., those that return Result{1, 0} or Result{0, 1}) to be similar. +// +// The exact ratio of NSame to NDiff to determine similarity may change. +func (r Result) Similar() bool { + // Use NSame+1 to offset NSame so that binary comparisons are similar. + return r.NSame+1 >= r.NDiff +} + +// Difference reports whether two lists of lengths nx and ny are equal +// given the definition of equality provided as f. +// +// This function returns an edit-script, which is a sequence of operations +// needed to convert one list into the other. The following invariants for +// the edit-script are maintained: +// • eq == (es.Dist()==0) +// • nx == es.LenX() +// • ny == es.LenY() +// +// This algorithm is not guaranteed to be an optimal solution (i.e., one that +// produces an edit-script with a minimal Levenshtein distance). This algorithm +// favors performance over optimality. The exact output is not guaranteed to +// be stable and may change over time. +func Difference(nx, ny int, f EqualFunc) (es EditScript) { + // This algorithm is based on traversing what is known as an "edit-graph". + // See Figure 1 from "An O(ND) Difference Algorithm and Its Variations" + // by Eugene W. Myers. Since D can be as large as N itself, this is + // effectively O(N^2). Unlike the algorithm from that paper, we are not + // interested in the optimal path, but at least some "decent" path. + // + // For example, let X and Y be lists of symbols: + // X = [A B C A B B A] + // Y = [C B A B A C] + // + // The edit-graph can be drawn as the following: + // A B C A B B A + // ┌─────────────┐ + // C │_|_|\|_|_|_|_│ 0 + // B │_|\|_|_|\|\|_│ 1 + // A │\|_|_|\|_|_|\│ 2 + // B │_|\|_|_|\|\|_│ 3 + // A │\|_|_|\|_|_|\│ 4 + // C │ | |\| | | | │ 5 + // └─────────────┘ 6 + // 0 1 2 3 4 5 6 7 + // + // List X is written along the horizontal axis, while list Y is written + // along the vertical axis. At any point on this grid, if the symbol in + // list X matches the corresponding symbol in list Y, then a '\' is drawn. + // The goal of any minimal edit-script algorithm is to find a path from the + // top-left corner to the bottom-right corner, while traveling through the + // fewest horizontal or vertical edges. + // A horizontal edge is equivalent to inserting a symbol from list X. + // A vertical edge is equivalent to inserting a symbol from list Y. + // A diagonal edge is equivalent to a matching symbol between both X and Y. + + // Invariants: + // • 0 ≤ fwdPath.X ≤ (fwdFrontier.X, revFrontier.X) ≤ revPath.X ≤ nx + // • 0 ≤ fwdPath.Y ≤ (fwdFrontier.Y, revFrontier.Y) ≤ revPath.Y ≤ ny + // + // In general: + // • fwdFrontier.X < revFrontier.X + // • fwdFrontier.Y < revFrontier.Y + // Unless, it is time for the algorithm to terminate. + fwdPath := path{+1, point{0, 0}, make(EditScript, 0, (nx+ny)/2)} + revPath := path{-1, point{nx, ny}, make(EditScript, 0)} + fwdFrontier := fwdPath.point // Forward search frontier + revFrontier := revPath.point // Reverse search frontier + + // Search budget bounds the cost of searching for better paths. + // The longest sequence of non-matching symbols that can be tolerated is + // approximately the square-root of the search budget. + searchBudget := 4 * (nx + ny) // O(n) + + // The algorithm below is a greedy, meet-in-the-middle algorithm for + // computing sub-optimal edit-scripts between two lists. + // + // The algorithm is approximately as follows: + // • Searching for differences switches back-and-forth between + // a search that starts at the beginning (the top-left corner), and + // a search that starts at the end (the bottom-right corner). The goal of + // the search is connect with the search from the opposite corner. + // • As we search, we build a path in a greedy manner, where the first + // match seen is added to the path (this is sub-optimal, but provides a + // decent result in practice). When matches are found, we try the next pair + // of symbols in the lists and follow all matches as far as possible. + // • When searching for matches, we search along a diagonal going through + // through the "frontier" point. If no matches are found, we advance the + // frontier towards the opposite corner. + // • This algorithm terminates when either the X coordinates or the + // Y coordinates of the forward and reverse frontier points ever intersect. + // + // This algorithm is correct even if searching only in the forward direction + // or in the reverse direction. We do both because it is commonly observed + // that two lists commonly differ because elements were added to the front + // or end of the other list. + // + // Running the tests with the "debug" build tag prints a visualization of + // the algorithm running in real-time. This is educational for understanding + // how the algorithm works. See debug_enable.go. + f = debug.Begin(nx, ny, f, &fwdPath.es, &revPath.es) + for { + // Forward search from the beginning. + if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 { + break + } + for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ { + // Search in a diagonal pattern for a match. + z := zigzag(i) + p := point{fwdFrontier.X + z, fwdFrontier.Y - z} + switch { + case p.X >= revPath.X || p.Y < fwdPath.Y: + stop1 = true // Hit top-right corner + case p.Y >= revPath.Y || p.X < fwdPath.X: + stop2 = true // Hit bottom-left corner + case f(p.X, p.Y).Equal(): + // Match found, so connect the path to this point. + fwdPath.connect(p, f) + fwdPath.append(Identity) + // Follow sequence of matches as far as possible. + for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y { + if !f(fwdPath.X, fwdPath.Y).Equal() { + break + } + fwdPath.append(Identity) + } + fwdFrontier = fwdPath.point + stop1, stop2 = true, true + default: + searchBudget-- // Match not found + } + debug.Update() + } + // Advance the frontier towards reverse point. + if revPath.X-fwdFrontier.X >= revPath.Y-fwdFrontier.Y { + fwdFrontier.X++ + } else { + fwdFrontier.Y++ + } + + // Reverse search from the end. + if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 { + break + } + for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ { + // Search in a diagonal pattern for a match. + z := zigzag(i) + p := point{revFrontier.X - z, revFrontier.Y + z} + switch { + case fwdPath.X >= p.X || revPath.Y < p.Y: + stop1 = true // Hit bottom-left corner + case fwdPath.Y >= p.Y || revPath.X < p.X: + stop2 = true // Hit top-right corner + case f(p.X-1, p.Y-1).Equal(): + // Match found, so connect the path to this point. + revPath.connect(p, f) + revPath.append(Identity) + // Follow sequence of matches as far as possible. + for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y { + if !f(revPath.X-1, revPath.Y-1).Equal() { + break + } + revPath.append(Identity) + } + revFrontier = revPath.point + stop1, stop2 = true, true + default: + searchBudget-- // Match not found + } + debug.Update() + } + // Advance the frontier towards forward point. + if revFrontier.X-fwdPath.X >= revFrontier.Y-fwdPath.Y { + revFrontier.X-- + } else { + revFrontier.Y-- + } + } + + // Join the forward and reverse paths and then append the reverse path. + fwdPath.connect(revPath.point, f) + for i := len(revPath.es) - 1; i >= 0; i-- { + t := revPath.es[i] + revPath.es = revPath.es[:i] + fwdPath.append(t) + } + debug.Finish() + return fwdPath.es +} + +type path struct { + dir int // +1 if forward, -1 if reverse + point // Leading point of the EditScript path + es EditScript +} + +// connect appends any necessary Identity, Modified, UniqueX, or UniqueY types +// to the edit-script to connect p.point to dst. +func (p *path) connect(dst point, f EqualFunc) { + if p.dir > 0 { + // Connect in forward direction. + for dst.X > p.X && dst.Y > p.Y { + switch r := f(p.X, p.Y); { + case r.Equal(): + p.append(Identity) + case r.Similar(): + p.append(Modified) + case dst.X-p.X >= dst.Y-p.Y: + p.append(UniqueX) + default: + p.append(UniqueY) + } + } + for dst.X > p.X { + p.append(UniqueX) + } + for dst.Y > p.Y { + p.append(UniqueY) + } + } else { + // Connect in reverse direction. + for p.X > dst.X && p.Y > dst.Y { + switch r := f(p.X-1, p.Y-1); { + case r.Equal(): + p.append(Identity) + case r.Similar(): + p.append(Modified) + case p.Y-dst.Y >= p.X-dst.X: + p.append(UniqueY) + default: + p.append(UniqueX) + } + } + for p.X > dst.X { + p.append(UniqueX) + } + for p.Y > dst.Y { + p.append(UniqueY) + } + } +} + +func (p *path) append(t EditType) { + p.es = append(p.es, t) + switch t { + case Identity, Modified: + p.add(p.dir, p.dir) + case UniqueX: + p.add(p.dir, 0) + case UniqueY: + p.add(0, p.dir) + } + debug.Update() +} + +type point struct{ X, Y int } + +func (p *point) add(dx, dy int) { p.X += dx; p.Y += dy } + +// zigzag maps a consecutive sequence of integers to a zig-zag sequence. +// [0 1 2 3 4 5 ...] => [0 -1 +1 -2 +2 ...] +func zigzag(x int) int { + if x&1 != 0 { + x = ^x + } + return x >> 1 +} diff --git a/components/cli/vendor/github.com/google/go-cmp/cmp/internal/function/func.go b/components/cli/vendor/github.com/google/go-cmp/cmp/internal/function/func.go new file mode 100644 index 0000000000..4c35ff11ee --- /dev/null +++ b/components/cli/vendor/github.com/google/go-cmp/cmp/internal/function/func.go @@ -0,0 +1,49 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// Package function identifies function types. +package function + +import "reflect" + +type funcType int + +const ( + _ funcType = iota + + ttbFunc // func(T, T) bool + tibFunc // func(T, I) bool + trFunc // func(T) R + + Equal = ttbFunc // func(T, T) bool + EqualAssignable = tibFunc // func(T, I) bool; encapsulates func(T, T) bool + Transformer = trFunc // func(T) R + ValueFilter = ttbFunc // func(T, T) bool + Less = ttbFunc // func(T, T) bool +) + +var boolType = reflect.TypeOf(true) + +// IsType reports whether the reflect.Type is of the specified function type. +func IsType(t reflect.Type, ft funcType) bool { + if t == nil || t.Kind() != reflect.Func || t.IsVariadic() { + return false + } + ni, no := t.NumIn(), t.NumOut() + switch ft { + case ttbFunc: // func(T, T) bool + if ni == 2 && no == 1 && t.In(0) == t.In(1) && t.Out(0) == boolType { + return true + } + case tibFunc: // func(T, I) bool + if ni == 2 && no == 1 && t.In(0).AssignableTo(t.In(1)) && t.Out(0) == boolType { + return true + } + case trFunc: // func(T) R + if ni == 1 && no == 1 { + return true + } + } + return false +} diff --git a/components/cli/vendor/github.com/google/go-cmp/cmp/internal/value/format.go b/components/cli/vendor/github.com/google/go-cmp/cmp/internal/value/format.go new file mode 100644 index 0000000000..657e508779 --- /dev/null +++ b/components/cli/vendor/github.com/google/go-cmp/cmp/internal/value/format.go @@ -0,0 +1,277 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// Package value provides functionality for reflect.Value types. +package value + +import ( + "fmt" + "reflect" + "strconv" + "strings" + "unicode" +) + +var stringerIface = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() + +// Format formats the value v as a string. +// +// This is similar to fmt.Sprintf("%+v", v) except this: +// * Prints the type unless it can be elided +// * Avoids printing struct fields that are zero +// * Prints a nil-slice as being nil, not empty +// * Prints map entries in deterministic order +func Format(v reflect.Value, conf FormatConfig) string { + conf.printType = true + conf.followPointers = true + conf.realPointers = true + return formatAny(v, conf, nil) +} + +type FormatConfig struct { + UseStringer bool // Should the String method be used if available? + printType bool // Should we print the type before the value? + PrintPrimitiveType bool // Should we print the type of primitives? + followPointers bool // Should we recursively follow pointers? + realPointers bool // Should we print the real address of pointers? +} + +func formatAny(v reflect.Value, conf FormatConfig, visited map[uintptr]bool) string { + // TODO: Should this be a multi-line printout in certain situations? + + if !v.IsValid() { + return "" + } + if conf.UseStringer && v.Type().Implements(stringerIface) && v.CanInterface() { + if (v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface) && v.IsNil() { + return "" + } + + const stringerPrefix = "s" // Indicates that the String method was used + s := v.Interface().(fmt.Stringer).String() + return stringerPrefix + formatString(s) + } + + switch v.Kind() { + case reflect.Bool: + return formatPrimitive(v.Type(), v.Bool(), conf) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return formatPrimitive(v.Type(), v.Int(), conf) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + if v.Type().PkgPath() == "" || v.Kind() == reflect.Uintptr { + // Unnamed uints are usually bytes or words, so use hexadecimal. + return formatPrimitive(v.Type(), formatHex(v.Uint()), conf) + } + return formatPrimitive(v.Type(), v.Uint(), conf) + case reflect.Float32, reflect.Float64: + return formatPrimitive(v.Type(), v.Float(), conf) + case reflect.Complex64, reflect.Complex128: + return formatPrimitive(v.Type(), v.Complex(), conf) + case reflect.String: + return formatPrimitive(v.Type(), formatString(v.String()), conf) + case reflect.UnsafePointer, reflect.Chan, reflect.Func: + return formatPointer(v, conf) + case reflect.Ptr: + if v.IsNil() { + if conf.printType { + return fmt.Sprintf("(%v)(nil)", v.Type()) + } + return "" + } + if visited[v.Pointer()] || !conf.followPointers { + return formatPointer(v, conf) + } + visited = insertPointer(visited, v.Pointer()) + return "&" + formatAny(v.Elem(), conf, visited) + case reflect.Interface: + if v.IsNil() { + if conf.printType { + return fmt.Sprintf("%v(nil)", v.Type()) + } + return "" + } + return formatAny(v.Elem(), conf, visited) + case reflect.Slice: + if v.IsNil() { + if conf.printType { + return fmt.Sprintf("%v(nil)", v.Type()) + } + return "" + } + if visited[v.Pointer()] { + return formatPointer(v, conf) + } + visited = insertPointer(visited, v.Pointer()) + fallthrough + case reflect.Array: + var ss []string + subConf := conf + subConf.printType = v.Type().Elem().Kind() == reflect.Interface + for i := 0; i < v.Len(); i++ { + s := formatAny(v.Index(i), subConf, visited) + ss = append(ss, s) + } + s := fmt.Sprintf("{%s}", strings.Join(ss, ", ")) + if conf.printType { + return v.Type().String() + s + } + return s + case reflect.Map: + if v.IsNil() { + if conf.printType { + return fmt.Sprintf("%v(nil)", v.Type()) + } + return "" + } + if visited[v.Pointer()] { + return formatPointer(v, conf) + } + visited = insertPointer(visited, v.Pointer()) + + var ss []string + keyConf, valConf := conf, conf + keyConf.printType = v.Type().Key().Kind() == reflect.Interface + keyConf.followPointers = false + valConf.printType = v.Type().Elem().Kind() == reflect.Interface + for _, k := range SortKeys(v.MapKeys()) { + sk := formatAny(k, keyConf, visited) + sv := formatAny(v.MapIndex(k), valConf, visited) + ss = append(ss, fmt.Sprintf("%s: %s", sk, sv)) + } + s := fmt.Sprintf("{%s}", strings.Join(ss, ", ")) + if conf.printType { + return v.Type().String() + s + } + return s + case reflect.Struct: + var ss []string + subConf := conf + subConf.printType = true + for i := 0; i < v.NumField(); i++ { + vv := v.Field(i) + if isZero(vv) { + continue // Elide zero value fields + } + name := v.Type().Field(i).Name + subConf.UseStringer = conf.UseStringer + s := formatAny(vv, subConf, visited) + ss = append(ss, fmt.Sprintf("%s: %s", name, s)) + } + s := fmt.Sprintf("{%s}", strings.Join(ss, ", ")) + if conf.printType { + return v.Type().String() + s + } + return s + default: + panic(fmt.Sprintf("%v kind not handled", v.Kind())) + } +} + +func formatString(s string) string { + // Use quoted string if it the same length as a raw string literal. + // Otherwise, attempt to use the raw string form. + qs := strconv.Quote(s) + if len(qs) == 1+len(s)+1 { + return qs + } + + // Disallow newlines to ensure output is a single line. + // Only allow printable runes for readability purposes. + rawInvalid := func(r rune) bool { + return r == '`' || r == '\n' || !unicode.IsPrint(r) + } + if strings.IndexFunc(s, rawInvalid) < 0 { + return "`" + s + "`" + } + return qs +} + +func formatPrimitive(t reflect.Type, v interface{}, conf FormatConfig) string { + if conf.printType && (conf.PrintPrimitiveType || t.PkgPath() != "") { + return fmt.Sprintf("%v(%v)", t, v) + } + return fmt.Sprintf("%v", v) +} + +func formatPointer(v reflect.Value, conf FormatConfig) string { + p := v.Pointer() + if !conf.realPointers { + p = 0 // For deterministic printing purposes + } + s := formatHex(uint64(p)) + if conf.printType { + return fmt.Sprintf("(%v)(%s)", v.Type(), s) + } + return s +} + +func formatHex(u uint64) string { + var f string + switch { + case u <= 0xff: + f = "0x%02x" + case u <= 0xffff: + f = "0x%04x" + case u <= 0xffffff: + f = "0x%06x" + case u <= 0xffffffff: + f = "0x%08x" + case u <= 0xffffffffff: + f = "0x%010x" + case u <= 0xffffffffffff: + f = "0x%012x" + case u <= 0xffffffffffffff: + f = "0x%014x" + case u <= 0xffffffffffffffff: + f = "0x%016x" + } + return fmt.Sprintf(f, u) +} + +// insertPointer insert p into m, allocating m if necessary. +func insertPointer(m map[uintptr]bool, p uintptr) map[uintptr]bool { + if m == nil { + m = make(map[uintptr]bool) + } + m[p] = true + return m +} + +// isZero reports whether v is the zero value. +// This does not rely on Interface and so can be used on unexported fields. +func isZero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Bool: + return v.Bool() == false + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Complex64, reflect.Complex128: + return v.Complex() == 0 + case reflect.String: + return v.String() == "" + case reflect.UnsafePointer: + return v.Pointer() == 0 + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: + return v.IsNil() + case reflect.Array: + for i := 0; i < v.Len(); i++ { + if !isZero(v.Index(i)) { + return false + } + } + return true + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + if !isZero(v.Field(i)) { + return false + } + } + return true + } + return false +} diff --git a/components/cli/vendor/github.com/google/go-cmp/cmp/internal/value/sort.go b/components/cli/vendor/github.com/google/go-cmp/cmp/internal/value/sort.go new file mode 100644 index 0000000000..fe8aa27a07 --- /dev/null +++ b/components/cli/vendor/github.com/google/go-cmp/cmp/internal/value/sort.go @@ -0,0 +1,111 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package value + +import ( + "fmt" + "math" + "reflect" + "sort" +) + +// SortKeys sorts a list of map keys, deduplicating keys if necessary. +// The type of each value must be comparable. +func SortKeys(vs []reflect.Value) []reflect.Value { + if len(vs) == 0 { + return vs + } + + // Sort the map keys. + sort.Sort(valueSorter(vs)) + + // Deduplicate keys (fails for NaNs). + vs2 := vs[:1] + for _, v := range vs[1:] { + if isLess(vs2[len(vs2)-1], v) { + vs2 = append(vs2, v) + } + } + return vs2 +} + +// TODO: Use sort.Slice once Google AppEngine is on Go1.8 or above. +type valueSorter []reflect.Value + +func (vs valueSorter) Len() int { return len(vs) } +func (vs valueSorter) Less(i, j int) bool { return isLess(vs[i], vs[j]) } +func (vs valueSorter) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] } + +// isLess is a generic function for sorting arbitrary map keys. +// The inputs must be of the same type and must be comparable. +func isLess(x, y reflect.Value) bool { + switch x.Type().Kind() { + case reflect.Bool: + return !x.Bool() && y.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return x.Int() < y.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return x.Uint() < y.Uint() + case reflect.Float32, reflect.Float64: + fx, fy := x.Float(), y.Float() + return fx < fy || math.IsNaN(fx) && !math.IsNaN(fy) + case reflect.Complex64, reflect.Complex128: + cx, cy := x.Complex(), y.Complex() + rx, ix, ry, iy := real(cx), imag(cx), real(cy), imag(cy) + if rx == ry || (math.IsNaN(rx) && math.IsNaN(ry)) { + return ix < iy || math.IsNaN(ix) && !math.IsNaN(iy) + } + return rx < ry || math.IsNaN(rx) && !math.IsNaN(ry) + case reflect.Ptr, reflect.UnsafePointer, reflect.Chan: + return x.Pointer() < y.Pointer() + case reflect.String: + return x.String() < y.String() + case reflect.Array: + for i := 0; i < x.Len(); i++ { + if isLess(x.Index(i), y.Index(i)) { + return true + } + if isLess(y.Index(i), x.Index(i)) { + return false + } + } + return false + case reflect.Struct: + for i := 0; i < x.NumField(); i++ { + if isLess(x.Field(i), y.Field(i)) { + return true + } + if isLess(y.Field(i), x.Field(i)) { + return false + } + } + return false + case reflect.Interface: + vx, vy := x.Elem(), y.Elem() + if !vx.IsValid() || !vy.IsValid() { + return !vx.IsValid() && vy.IsValid() + } + tx, ty := vx.Type(), vy.Type() + if tx == ty { + return isLess(x.Elem(), y.Elem()) + } + if tx.Kind() != ty.Kind() { + return vx.Kind() < vy.Kind() + } + if tx.String() != ty.String() { + return tx.String() < ty.String() + } + if tx.PkgPath() != ty.PkgPath() { + return tx.PkgPath() < ty.PkgPath() + } + // This can happen in rare situations, so we fallback to just comparing + // the unique pointer for a reflect.Type. This guarantees deterministic + // ordering within a program, but it is obviously not stable. + return reflect.ValueOf(vx.Type()).Pointer() < reflect.ValueOf(vy.Type()).Pointer() + default: + // Must be Func, Map, or Slice; which are not comparable. + panic(fmt.Sprintf("%T is not comparable", x.Type())) + } +} diff --git a/components/cli/vendor/github.com/google/go-cmp/cmp/options.go b/components/cli/vendor/github.com/google/go-cmp/cmp/options.go new file mode 100644 index 0000000000..91d4b066e0 --- /dev/null +++ b/components/cli/vendor/github.com/google/go-cmp/cmp/options.go @@ -0,0 +1,453 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package cmp + +import ( + "fmt" + "reflect" + "runtime" + "strings" + + "github.com/google/go-cmp/cmp/internal/function" +) + +// Option configures for specific behavior of Equal and Diff. In particular, +// the fundamental Option functions (Ignore, Transformer, and Comparer), +// configure how equality is determined. +// +// The fundamental options may be composed with filters (FilterPath and +// FilterValues) to control the scope over which they are applied. +// +// The cmp/cmpopts package provides helper functions for creating options that +// may be used with Equal and Diff. +type Option interface { + // filter applies all filters and returns the option that remains. + // Each option may only read s.curPath and call s.callTTBFunc. + // + // An Options is returned only if multiple comparers or transformers + // can apply simultaneously and will only contain values of those types + // or sub-Options containing values of those types. + filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption +} + +// applicableOption represents the following types: +// Fundamental: ignore | invalid | *comparer | *transformer +// Grouping: Options +type applicableOption interface { + Option + + // apply executes the option, which may mutate s or panic. + apply(s *state, vx, vy reflect.Value) +} + +// coreOption represents the following types: +// Fundamental: ignore | invalid | *comparer | *transformer +// Filters: *pathFilter | *valuesFilter +type coreOption interface { + Option + isCore() +} + +type core struct{} + +func (core) isCore() {} + +// Options is a list of Option values that also satisfies the Option interface. +// Helper comparison packages may return an Options value when packing multiple +// Option values into a single Option. When this package processes an Options, +// it will be implicitly expanded into a flat list. +// +// Applying a filter on an Options is equivalent to applying that same filter +// on all individual options held within. +type Options []Option + +func (opts Options) filter(s *state, vx, vy reflect.Value, t reflect.Type) (out applicableOption) { + for _, opt := range opts { + switch opt := opt.filter(s, vx, vy, t); opt.(type) { + case ignore: + return ignore{} // Only ignore can short-circuit evaluation + case invalid: + out = invalid{} // Takes precedence over comparer or transformer + case *comparer, *transformer, Options: + switch out.(type) { + case nil: + out = opt + case invalid: + // Keep invalid + case *comparer, *transformer, Options: + out = Options{out, opt} // Conflicting comparers or transformers + } + } + } + return out +} + +func (opts Options) apply(s *state, _, _ reflect.Value) { + const warning = "ambiguous set of applicable options" + const help = "consider using filters to ensure at most one Comparer or Transformer may apply" + var ss []string + for _, opt := range flattenOptions(nil, opts) { + ss = append(ss, fmt.Sprint(opt)) + } + set := strings.Join(ss, "\n\t") + panic(fmt.Sprintf("%s at %#v:\n\t%s\n%s", warning, s.curPath, set, help)) +} + +func (opts Options) String() string { + var ss []string + for _, opt := range opts { + ss = append(ss, fmt.Sprint(opt)) + } + return fmt.Sprintf("Options{%s}", strings.Join(ss, ", ")) +} + +// FilterPath returns a new Option where opt is only evaluated if filter f +// returns true for the current Path in the value tree. +// +// The option passed in may be an Ignore, Transformer, Comparer, Options, or +// a previously filtered Option. +func FilterPath(f func(Path) bool, opt Option) Option { + if f == nil { + panic("invalid path filter function") + } + if opt := normalizeOption(opt); opt != nil { + return &pathFilter{fnc: f, opt: opt} + } + return nil +} + +type pathFilter struct { + core + fnc func(Path) bool + opt Option +} + +func (f pathFilter) filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption { + if f.fnc(s.curPath) { + return f.opt.filter(s, vx, vy, t) + } + return nil +} + +func (f pathFilter) String() string { + fn := getFuncName(reflect.ValueOf(f.fnc).Pointer()) + return fmt.Sprintf("FilterPath(%s, %v)", fn, f.opt) +} + +// FilterValues returns a new Option where opt is only evaluated if filter f, +// which is a function of the form "func(T, T) bool", returns true for the +// current pair of values being compared. If the type of the values is not +// assignable to T, then this filter implicitly returns false. +// +// The filter function must be +// symmetric (i.e., agnostic to the order of the inputs) and +// deterministic (i.e., produces the same result when given the same inputs). +// If T is an interface, it is possible that f is called with two values with +// different concrete types that both implement T. +// +// The option passed in may be an Ignore, Transformer, Comparer, Options, or +// a previously filtered Option. +func FilterValues(f interface{}, opt Option) Option { + v := reflect.ValueOf(f) + if !function.IsType(v.Type(), function.ValueFilter) || v.IsNil() { + panic(fmt.Sprintf("invalid values filter function: %T", f)) + } + if opt := normalizeOption(opt); opt != nil { + vf := &valuesFilter{fnc: v, opt: opt} + if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { + vf.typ = ti + } + return vf + } + return nil +} + +type valuesFilter struct { + core + typ reflect.Type // T + fnc reflect.Value // func(T, T) bool + opt Option +} + +func (f valuesFilter) filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption { + if !vx.IsValid() || !vy.IsValid() { + return invalid{} + } + if (f.typ == nil || t.AssignableTo(f.typ)) && s.callTTBFunc(f.fnc, vx, vy) { + return f.opt.filter(s, vx, vy, t) + } + return nil +} + +func (f valuesFilter) String() string { + fn := getFuncName(f.fnc.Pointer()) + return fmt.Sprintf("FilterValues(%s, %v)", fn, f.opt) +} + +// Ignore is an Option that causes all comparisons to be ignored. +// This value is intended to be combined with FilterPath or FilterValues. +// It is an error to pass an unfiltered Ignore option to Equal. +func Ignore() Option { return ignore{} } + +type ignore struct{ core } + +func (ignore) isFiltered() bool { return false } +func (ignore) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return ignore{} } +func (ignore) apply(_ *state, _, _ reflect.Value) { return } +func (ignore) String() string { return "Ignore()" } + +// invalid is a sentinel Option type to indicate that some options could not +// be evaluated due to unexported fields. +type invalid struct{ core } + +func (invalid) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return invalid{} } +func (invalid) apply(s *state, _, _ reflect.Value) { + const help = "consider using AllowUnexported or cmpopts.IgnoreUnexported" + panic(fmt.Sprintf("cannot handle unexported field: %#v\n%s", s.curPath, help)) +} + +// Transformer returns an Option that applies a transformation function that +// converts values of a certain type into that of another. +// +// The transformer f must be a function "func(T) R" that converts values of +// type T to those of type R and is implicitly filtered to input values +// assignable to T. The transformer must not mutate T in any way. +// +// To help prevent some cases of infinite recursive cycles applying the +// same transform to the output of itself (e.g., in the case where the +// input and output types are the same), an implicit filter is added such that +// a transformer is applicable only if that exact transformer is not already +// in the tail of the Path since the last non-Transform step. +// +// The name is a user provided label that is used as the Transform.Name in the +// transformation PathStep. If empty, an arbitrary name is used. +func Transformer(name string, f interface{}) Option { + v := reflect.ValueOf(f) + if !function.IsType(v.Type(), function.Transformer) || v.IsNil() { + panic(fmt.Sprintf("invalid transformer function: %T", f)) + } + if name == "" { + name = "λ" // Lambda-symbol as place-holder for anonymous transformer + } + if !isValid(name) { + panic(fmt.Sprintf("invalid name: %q", name)) + } + tr := &transformer{name: name, fnc: reflect.ValueOf(f)} + if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { + tr.typ = ti + } + return tr +} + +type transformer struct { + core + name string + typ reflect.Type // T + fnc reflect.Value // func(T) R +} + +func (tr *transformer) isFiltered() bool { return tr.typ != nil } + +func (tr *transformer) filter(s *state, _, _ reflect.Value, t reflect.Type) applicableOption { + for i := len(s.curPath) - 1; i >= 0; i-- { + if t, ok := s.curPath[i].(*transform); !ok { + break // Hit most recent non-Transform step + } else if tr == t.trans { + return nil // Cannot directly use same Transform + } + } + if tr.typ == nil || t.AssignableTo(tr.typ) { + return tr + } + return nil +} + +func (tr *transformer) apply(s *state, vx, vy reflect.Value) { + // Update path before calling the Transformer so that dynamic checks + // will use the updated path. + s.curPath.push(&transform{pathStep{tr.fnc.Type().Out(0)}, tr}) + defer s.curPath.pop() + + vx = s.callTRFunc(tr.fnc, vx) + vy = s.callTRFunc(tr.fnc, vy) + s.compareAny(vx, vy) +} + +func (tr transformer) String() string { + return fmt.Sprintf("Transformer(%s, %s)", tr.name, getFuncName(tr.fnc.Pointer())) +} + +// Comparer returns an Option that determines whether two values are equal +// to each other. +// +// The comparer f must be a function "func(T, T) bool" and is implicitly +// filtered to input values assignable to T. If T is an interface, it is +// possible that f is called with two values of different concrete types that +// both implement T. +// +// The equality function must be: +// • Symmetric: equal(x, y) == equal(y, x) +// • Deterministic: equal(x, y) == equal(x, y) +// • Pure: equal(x, y) does not modify x or y +func Comparer(f interface{}) Option { + v := reflect.ValueOf(f) + if !function.IsType(v.Type(), function.Equal) || v.IsNil() { + panic(fmt.Sprintf("invalid comparer function: %T", f)) + } + cm := &comparer{fnc: v} + if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { + cm.typ = ti + } + return cm +} + +type comparer struct { + core + typ reflect.Type // T + fnc reflect.Value // func(T, T) bool +} + +func (cm *comparer) isFiltered() bool { return cm.typ != nil } + +func (cm *comparer) filter(_ *state, _, _ reflect.Value, t reflect.Type) applicableOption { + if cm.typ == nil || t.AssignableTo(cm.typ) { + return cm + } + return nil +} + +func (cm *comparer) apply(s *state, vx, vy reflect.Value) { + eq := s.callTTBFunc(cm.fnc, vx, vy) + s.report(eq, vx, vy) +} + +func (cm comparer) String() string { + return fmt.Sprintf("Comparer(%s)", getFuncName(cm.fnc.Pointer())) +} + +// AllowUnexported returns an Option that forcibly allows operations on +// unexported fields in certain structs, which are specified by passing in a +// value of each struct type. +// +// Users of this option must understand that comparing on unexported fields +// from external packages is not safe since changes in the internal +// implementation of some external package may cause the result of Equal +// to unexpectedly change. However, it may be valid to use this option on types +// defined in an internal package where the semantic meaning of an unexported +// field is in the control of the user. +// +// For some cases, a custom Comparer should be used instead that defines +// equality as a function of the public API of a type rather than the underlying +// unexported implementation. +// +// For example, the reflect.Type documentation defines equality to be determined +// by the == operator on the interface (essentially performing a shallow pointer +// comparison) and most attempts to compare *regexp.Regexp types are interested +// in only checking that the regular expression strings are equal. +// Both of these are accomplished using Comparers: +// +// Comparer(func(x, y reflect.Type) bool { return x == y }) +// Comparer(func(x, y *regexp.Regexp) bool { return x.String() == y.String() }) +// +// In other cases, the cmpopts.IgnoreUnexported option can be used to ignore +// all unexported fields on specified struct types. +func AllowUnexported(types ...interface{}) Option { + if !supportAllowUnexported { + panic("AllowUnexported is not supported on purego builds, Google App Engine Standard, or GopherJS") + } + m := make(map[reflect.Type]bool) + for _, typ := range types { + t := reflect.TypeOf(typ) + if t.Kind() != reflect.Struct { + panic(fmt.Sprintf("invalid struct type: %T", typ)) + } + m[t] = true + } + return visibleStructs(m) +} + +type visibleStructs map[reflect.Type]bool + +func (visibleStructs) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { + panic("not implemented") +} + +// reporter is an Option that configures how differences are reported. +type reporter interface { + // TODO: Not exported yet. + // + // Perhaps add PushStep and PopStep and change Report to only accept + // a PathStep instead of the full-path? Adding a PushStep and PopStep makes + // it clear that we are traversing the value tree in a depth-first-search + // manner, which has an effect on how values are printed. + + Option + + // Report is called for every comparison made and will be provided with + // the two values being compared, the equality result, and the + // current path in the value tree. It is possible for x or y to be an + // invalid reflect.Value if one of the values is non-existent; + // which is possible with maps and slices. + Report(x, y reflect.Value, eq bool, p Path) +} + +// normalizeOption normalizes the input options such that all Options groups +// are flattened and groups with a single element are reduced to that element. +// Only coreOptions and Options containing coreOptions are allowed. +func normalizeOption(src Option) Option { + switch opts := flattenOptions(nil, Options{src}); len(opts) { + case 0: + return nil + case 1: + return opts[0] + default: + return opts + } +} + +// flattenOptions copies all options in src to dst as a flat list. +// Only coreOptions and Options containing coreOptions are allowed. +func flattenOptions(dst, src Options) Options { + for _, opt := range src { + switch opt := opt.(type) { + case nil: + continue + case Options: + dst = flattenOptions(dst, opt) + case coreOption: + dst = append(dst, opt) + default: + panic(fmt.Sprintf("invalid option type: %T", opt)) + } + } + return dst +} + +// getFuncName returns a short function name from the pointer. +// The string parsing logic works up until Go1.9. +func getFuncName(p uintptr) string { + fnc := runtime.FuncForPC(p) + if fnc == nil { + return "" + } + name := fnc.Name() // E.g., "long/path/name/mypkg.(mytype).(long/path/name/mypkg.myfunc)-fm" + if strings.HasSuffix(name, ")-fm") || strings.HasSuffix(name, ")·fm") { + // Strip the package name from method name. + name = strings.TrimSuffix(name, ")-fm") + name = strings.TrimSuffix(name, ")·fm") + if i := strings.LastIndexByte(name, '('); i >= 0 { + methodName := name[i+1:] // E.g., "long/path/name/mypkg.myfunc" + if j := strings.LastIndexByte(methodName, '.'); j >= 0 { + methodName = methodName[j+1:] // E.g., "myfunc" + } + name = name[:i] + methodName // E.g., "long/path/name/mypkg.(mytype)." + "myfunc" + } + } + if i := strings.LastIndexByte(name, '/'); i >= 0 { + // Strip the package name. + name = name[i+1:] // E.g., "mypkg.(mytype).myfunc" + } + return name +} diff --git a/components/cli/vendor/github.com/google/go-cmp/cmp/path.go b/components/cli/vendor/github.com/google/go-cmp/cmp/path.go new file mode 100644 index 0000000000..c08a3cf80d --- /dev/null +++ b/components/cli/vendor/github.com/google/go-cmp/cmp/path.go @@ -0,0 +1,309 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package cmp + +import ( + "fmt" + "reflect" + "strings" + "unicode" + "unicode/utf8" +) + +type ( + // Path is a list of PathSteps describing the sequence of operations to get + // from some root type to the current position in the value tree. + // The first Path element is always an operation-less PathStep that exists + // simply to identify the initial type. + // + // When traversing structs with embedded structs, the embedded struct will + // always be accessed as a field before traversing the fields of the + // embedded struct themselves. That is, an exported field from the + // embedded struct will never be accessed directly from the parent struct. + Path []PathStep + + // PathStep is a union-type for specific operations to traverse + // a value's tree structure. Users of this package never need to implement + // these types as values of this type will be returned by this package. + PathStep interface { + String() string + Type() reflect.Type // Resulting type after performing the path step + isPathStep() + } + + // SliceIndex is an index operation on a slice or array at some index Key. + SliceIndex interface { + PathStep + Key() int // May return -1 if in a split state + + // SplitKeys returns the indexes for indexing into slices in the + // x and y values, respectively. These indexes may differ due to the + // insertion or removal of an element in one of the slices, causing + // all of the indexes to be shifted. If an index is -1, then that + // indicates that the element does not exist in the associated slice. + // + // Key is guaranteed to return -1 if and only if the indexes returned + // by SplitKeys are not the same. SplitKeys will never return -1 for + // both indexes. + SplitKeys() (x int, y int) + + isSliceIndex() + } + // MapIndex is an index operation on a map at some index Key. + MapIndex interface { + PathStep + Key() reflect.Value + isMapIndex() + } + // TypeAssertion represents a type assertion on an interface. + TypeAssertion interface { + PathStep + isTypeAssertion() + } + // StructField represents a struct field access on a field called Name. + StructField interface { + PathStep + Name() string + Index() int + isStructField() + } + // Indirect represents pointer indirection on the parent type. + Indirect interface { + PathStep + isIndirect() + } + // Transform is a transformation from the parent type to the current type. + Transform interface { + PathStep + Name() string + Func() reflect.Value + + // Option returns the originally constructed Transformer option. + // The == operator can be used to detect the exact option used. + Option() Option + + isTransform() + } +) + +func (pa *Path) push(s PathStep) { + *pa = append(*pa, s) +} + +func (pa *Path) pop() { + *pa = (*pa)[:len(*pa)-1] +} + +// Last returns the last PathStep in the Path. +// If the path is empty, this returns a non-nil PathStep that reports a nil Type. +func (pa Path) Last() PathStep { + return pa.Index(-1) +} + +// Index returns the ith step in the Path and supports negative indexing. +// A negative index starts counting from the tail of the Path such that -1 +// refers to the last step, -2 refers to the second-to-last step, and so on. +// If index is invalid, this returns a non-nil PathStep that reports a nil Type. +func (pa Path) Index(i int) PathStep { + if i < 0 { + i = len(pa) + i + } + if i < 0 || i >= len(pa) { + return pathStep{} + } + return pa[i] +} + +// String returns the simplified path to a node. +// The simplified path only contains struct field accesses. +// +// For example: +// MyMap.MySlices.MyField +func (pa Path) String() string { + var ss []string + for _, s := range pa { + if _, ok := s.(*structField); ok { + ss = append(ss, s.String()) + } + } + return strings.TrimPrefix(strings.Join(ss, ""), ".") +} + +// GoString returns the path to a specific node using Go syntax. +// +// For example: +// (*root.MyMap["key"].(*mypkg.MyStruct).MySlices)[2][3].MyField +func (pa Path) GoString() string { + var ssPre, ssPost []string + var numIndirect int + for i, s := range pa { + var nextStep PathStep + if i+1 < len(pa) { + nextStep = pa[i+1] + } + switch s := s.(type) { + case *indirect: + numIndirect++ + pPre, pPost := "(", ")" + switch nextStep.(type) { + case *indirect: + continue // Next step is indirection, so let them batch up + case *structField: + numIndirect-- // Automatic indirection on struct fields + case nil: + pPre, pPost = "", "" // Last step; no need for parenthesis + } + if numIndirect > 0 { + ssPre = append(ssPre, pPre+strings.Repeat("*", numIndirect)) + ssPost = append(ssPost, pPost) + } + numIndirect = 0 + continue + case *transform: + ssPre = append(ssPre, s.trans.name+"(") + ssPost = append(ssPost, ")") + continue + case *typeAssertion: + // As a special-case, elide type assertions on anonymous types + // since they are typically generated dynamically and can be very + // verbose. For example, some transforms return interface{} because + // of Go's lack of generics, but typically take in and return the + // exact same concrete type. + if s.Type().PkgPath() == "" { + continue + } + } + ssPost = append(ssPost, s.String()) + } + for i, j := 0, len(ssPre)-1; i < j; i, j = i+1, j-1 { + ssPre[i], ssPre[j] = ssPre[j], ssPre[i] + } + return strings.Join(ssPre, "") + strings.Join(ssPost, "") +} + +type ( + pathStep struct { + typ reflect.Type + } + + sliceIndex struct { + pathStep + xkey, ykey int + } + mapIndex struct { + pathStep + key reflect.Value + } + typeAssertion struct { + pathStep + } + structField struct { + pathStep + name string + idx int + + // These fields are used for forcibly accessing an unexported field. + // pvx, pvy, and field are only valid if unexported is true. + unexported bool + force bool // Forcibly allow visibility + pvx, pvy reflect.Value // Parent values + field reflect.StructField // Field information + } + indirect struct { + pathStep + } + transform struct { + pathStep + trans *transformer + } +) + +func (ps pathStep) Type() reflect.Type { return ps.typ } +func (ps pathStep) String() string { + if ps.typ == nil { + return "" + } + s := ps.typ.String() + if s == "" || strings.ContainsAny(s, "{}\n") { + return "root" // Type too simple or complex to print + } + return fmt.Sprintf("{%s}", s) +} + +func (si sliceIndex) String() string { + switch { + case si.xkey == si.ykey: + return fmt.Sprintf("[%d]", si.xkey) + case si.ykey == -1: + // [5->?] means "I don't know where X[5] went" + return fmt.Sprintf("[%d->?]", si.xkey) + case si.xkey == -1: + // [?->3] means "I don't know where Y[3] came from" + return fmt.Sprintf("[?->%d]", si.ykey) + default: + // [5->3] means "X[5] moved to Y[3]" + return fmt.Sprintf("[%d->%d]", si.xkey, si.ykey) + } +} +func (mi mapIndex) String() string { return fmt.Sprintf("[%#v]", mi.key) } +func (ta typeAssertion) String() string { return fmt.Sprintf(".(%v)", ta.typ) } +func (sf structField) String() string { return fmt.Sprintf(".%s", sf.name) } +func (in indirect) String() string { return "*" } +func (tf transform) String() string { return fmt.Sprintf("%s()", tf.trans.name) } + +func (si sliceIndex) Key() int { + if si.xkey != si.ykey { + return -1 + } + return si.xkey +} +func (si sliceIndex) SplitKeys() (x, y int) { return si.xkey, si.ykey } +func (mi mapIndex) Key() reflect.Value { return mi.key } +func (sf structField) Name() string { return sf.name } +func (sf structField) Index() int { return sf.idx } +func (tf transform) Name() string { return tf.trans.name } +func (tf transform) Func() reflect.Value { return tf.trans.fnc } +func (tf transform) Option() Option { return tf.trans } + +func (pathStep) isPathStep() {} +func (sliceIndex) isSliceIndex() {} +func (mapIndex) isMapIndex() {} +func (typeAssertion) isTypeAssertion() {} +func (structField) isStructField() {} +func (indirect) isIndirect() {} +func (transform) isTransform() {} + +var ( + _ SliceIndex = sliceIndex{} + _ MapIndex = mapIndex{} + _ TypeAssertion = typeAssertion{} + _ StructField = structField{} + _ Indirect = indirect{} + _ Transform = transform{} + + _ PathStep = sliceIndex{} + _ PathStep = mapIndex{} + _ PathStep = typeAssertion{} + _ PathStep = structField{} + _ PathStep = indirect{} + _ PathStep = transform{} +) + +// isExported reports whether the identifier is exported. +func isExported(id string) bool { + r, _ := utf8.DecodeRuneInString(id) + return unicode.IsUpper(r) +} + +// isValid reports whether the identifier is valid. +// Empty and underscore-only strings are not valid. +func isValid(id string) bool { + ok := id != "" && id != "_" + for j, c := range id { + ok = ok && (j > 0 || !unicode.IsDigit(c)) + ok = ok && (c == '_' || unicode.IsLetter(c) || unicode.IsDigit(c)) + } + return ok +} diff --git a/components/cli/vendor/github.com/google/go-cmp/cmp/reporter.go b/components/cli/vendor/github.com/google/go-cmp/cmp/reporter.go new file mode 100644 index 0000000000..20e9f18e0d --- /dev/null +++ b/components/cli/vendor/github.com/google/go-cmp/cmp/reporter.go @@ -0,0 +1,53 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package cmp + +import ( + "fmt" + "reflect" + "strings" + + "github.com/google/go-cmp/cmp/internal/value" +) + +type defaultReporter struct { + Option + diffs []string // List of differences, possibly truncated + ndiffs int // Total number of differences + nbytes int // Number of bytes in diffs + nlines int // Number of lines in diffs +} + +var _ reporter = (*defaultReporter)(nil) + +func (r *defaultReporter) Report(x, y reflect.Value, eq bool, p Path) { + if eq { + return // Ignore equal results + } + const maxBytes = 4096 + const maxLines = 256 + r.ndiffs++ + if r.nbytes < maxBytes && r.nlines < maxLines { + sx := value.Format(x, value.FormatConfig{UseStringer: true}) + sy := value.Format(y, value.FormatConfig{UseStringer: true}) + if sx == sy { + // Unhelpful output, so use more exact formatting. + sx = value.Format(x, value.FormatConfig{PrintPrimitiveType: true}) + sy = value.Format(y, value.FormatConfig{PrintPrimitiveType: true}) + } + s := fmt.Sprintf("%#v:\n\t-: %s\n\t+: %s\n", p, sx, sy) + r.diffs = append(r.diffs, s) + r.nbytes += len(s) + r.nlines += strings.Count(s, "\n") + } +} + +func (r *defaultReporter) String() string { + s := strings.Join(r.diffs, "") + if r.ndiffs == len(r.diffs) { + return s + } + return fmt.Sprintf("%s... %d more differences ...", s, r.ndiffs-len(r.diffs)) +} diff --git a/components/cli/vendor/github.com/google/go-cmp/cmp/unsafe_panic.go b/components/cli/vendor/github.com/google/go-cmp/cmp/unsafe_panic.go new file mode 100644 index 0000000000..d1518eb3a8 --- /dev/null +++ b/components/cli/vendor/github.com/google/go-cmp/cmp/unsafe_panic.go @@ -0,0 +1,15 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// +build purego appengine js + +package cmp + +import "reflect" + +const supportAllowUnexported = false + +func unsafeRetrieveField(reflect.Value, reflect.StructField) reflect.Value { + panic("unsafeRetrieveField is not implemented") +} diff --git a/components/cli/vendor/github.com/google/go-cmp/cmp/unsafe_reflect.go b/components/cli/vendor/github.com/google/go-cmp/cmp/unsafe_reflect.go new file mode 100644 index 0000000000..579b65507f --- /dev/null +++ b/components/cli/vendor/github.com/google/go-cmp/cmp/unsafe_reflect.go @@ -0,0 +1,23 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// +build !purego,!appengine,!js + +package cmp + +import ( + "reflect" + "unsafe" +) + +const supportAllowUnexported = true + +// unsafeRetrieveField uses unsafe to forcibly retrieve any field from a struct +// such that the value has read-write permissions. +// +// The parent struct, v, must be addressable, while f must be a StructField +// describing the field to retrieve. +func unsafeRetrieveField(v reflect.Value, f reflect.StructField) reflect.Value { + return reflect.NewAt(f.Type, unsafe.Pointer(v.UnsafeAddr()+f.Offset)).Elem() +} diff --git a/components/cli/vendor/github.com/gotestyourself/gotestyourself/README.md b/components/cli/vendor/github.com/gotestyourself/gotestyourself/README.md index 037fba6c77..aad893ae03 100644 --- a/components/cli/vendor/github.com/gotestyourself/gotestyourself/README.md +++ b/components/cli/vendor/github.com/gotestyourself/gotestyourself/README.md @@ -10,24 +10,24 @@ patterns. ## Packages +* [assert](http://godoc.org/github.com/gotestyourself/gotestyourself/assert) - + compare values and fail the test when the comparison fails +* [env](http://godoc.org/github.com/gotestyourself/gotestyourself/env) - + test code that uses environment variables * [fs](http://godoc.org/github.com/gotestyourself/gotestyourself/fs) - create test files and directories * [golden](http://godoc.org/github.com/gotestyourself/gotestyourself/golden) - compare large multi-line strings -* [testsum](http://godoc.org/github.com/gotestyourself/gotestyourself/testsum) - - a program to summarize `go test` output and test failures * [icmd](http://godoc.org/github.com/gotestyourself/gotestyourself/icmd) - execute binaries and test the output * [poll](http://godoc.org/github.com/gotestyourself/gotestyourself/poll) - test asynchronous code by polling until a desired state is reached * [skip](http://godoc.org/github.com/gotestyourself/gotestyourself/skip) - skip tests based on conditions +* [testsum](http://godoc.org/github.com/gotestyourself/gotestyourself/testsum) - + a program to summarize `go test` output and test failures ## Related -* [testify/assert](https://godoc.org/github.com/stretchr/testify/assert) and - [testify/require](https://godoc.org/github.com/stretchr/testify/require) - - assertion libraries with common assertions -* [golang/mock](https://github.com/golang/mock) - generate mocks for interfaces -* [testify/suite](https://godoc.org/github.com/stretchr/testify/suite) - - group test into suites to share common setup/teardown logic +* [maxbrunsfeld/counterfeiter](https://github.com/maxbrunsfeld/counterfeiter) - generate fakes for interfaces +* [jonboulle/clockwork](https://github.com/jonboulle/clockwork) - a fake clock for testing code that uses `time` diff --git a/components/cli/vendor/github.com/gotestyourself/gotestyourself/assert/assert.go b/components/cli/vendor/github.com/gotestyourself/gotestyourself/assert/assert.go new file mode 100644 index 0000000000..1896618861 --- /dev/null +++ b/components/cli/vendor/github.com/gotestyourself/gotestyourself/assert/assert.go @@ -0,0 +1,250 @@ +/*Package assert provides assertions for comparing expected values to actual +values. When an assertion fails a helpful error message is printed. + +Assert and Check + +Assert() and Check() both accept a Comparison, and fail the test when the +comparison fails. The one difference is that Assert() will end the test execution +immediately (using t.FailNow()) whereas Check() will fail the test (using t.Fail()), +return the value of the comparison, then proceed with the rest of the test case. + +Example Usage + +The example below shows assert used with some common types. + + + import ( + "testing" + + "github.com/gotestyourself/gotestyourself/assert" + is "github.com/gotestyourself/gotestyourself/assert/cmp" + ) + + func TestEverything(t *testing.T) { + // booleans + assert.Assert(t, ok) + assert.Assert(t, !missing) + + // primitives + assert.Equal(t, count, 1) + assert.Equal(t, msg, "the message") + assert.Assert(t, total != 10) // NotEqual + + // errors + assert.NilError(t, closer.Close()) + assert.Assert(t, is.Error(err, "the exact error message")) + assert.Assert(t, is.ErrorContains(err, "includes this")) + assert.Assert(t, is.ErrorType(err, os.IsNotExist)) + + // complex types + assert.DeepEqual(t, result, myStruct{Name: "title"}) + assert.Assert(t, is.Len(items, 3)) + assert.Assert(t, len(sequence) != 0) // NotEmpty + assert.Assert(t, is.Contains(mapping, "key")) + + // pointers and interface + assert.Assert(t, is.Nil(ref)) + assert.Assert(t, ref != nil) // NotNil + } + +Comparisons + +https://godoc.org/github.com/gotestyourself/gotestyourself/assert/cmp provides +many common comparisons. Additional comparisons can be written to compare +values in other ways. See the example Assert (CustomComparison). + +*/ +package assert + +import ( + "fmt" + "go/ast" + "go/token" + + gocmp "github.com/google/go-cmp/cmp" + "github.com/gotestyourself/gotestyourself/assert/cmp" + "github.com/gotestyourself/gotestyourself/internal/format" + "github.com/gotestyourself/gotestyourself/internal/source" +) + +// BoolOrComparison can be a bool, or cmp.Comparison. See Assert() for usage. +type BoolOrComparison interface{} + +// TestingT is the subset of testing.T used by the assert package. +type TestingT interface { + FailNow() + Fail() + Log(args ...interface{}) +} + +type helperT interface { + Helper() +} + +const failureMessage = "assertion failed: " + +// nolint: gocyclo +func assert( + t TestingT, + failer func(), + argsFilter astExprListFilter, + comparison BoolOrComparison, + msgAndArgs ...interface{}, +) bool { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + var success bool + switch check := comparison.(type) { + case bool: + if check { + return true + } + logFailureFromBool(t, msgAndArgs...) + + // Undocumented legacy comparison without Result type + case func() (success bool, message string): + success = runCompareFunc(t, check, msgAndArgs...) + + case nil: + return true + + case error: + msg := "error is not nil: " + t.Log(format.WithCustomMessage(failureMessage+msg+check.Error(), msgAndArgs...)) + + case cmp.Comparison: + success = runComparison(t, argsFilter, check, msgAndArgs...) + + case func() cmp.Result: + success = runComparison(t, argsFilter, check, msgAndArgs...) + + default: + t.Log(fmt.Sprintf("invalid Comparison: %v (%T)", check, check)) + } + + if success { + return true + } + failer() + return false +} + +func runCompareFunc( + t TestingT, + f func() (success bool, message string), + msgAndArgs ...interface{}, +) bool { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if success, message := f(); !success { + t.Log(format.WithCustomMessage(failureMessage+message, msgAndArgs...)) + return false + } + return true +} + +func logFailureFromBool(t TestingT, msgAndArgs ...interface{}) { + const stackIndex = 3 // Assert()/Check(), assert(), formatFailureFromBool() + const comparisonArgPos = 1 + args, err := source.CallExprArgs(stackIndex) + if err != nil { + t.Log(err.Error()) + return + } + + msg, err := boolFailureMessage(args[comparisonArgPos]) + if err != nil { + t.Log(err.Error()) + msg = "expression is false" + } + + t.Log(format.WithCustomMessage(failureMessage+msg, msgAndArgs...)) +} + +func boolFailureMessage(expr ast.Expr) (string, error) { + if binaryExpr, ok := expr.(*ast.BinaryExpr); ok && binaryExpr.Op == token.NEQ { + x, err := source.FormatNode(binaryExpr.X) + if err != nil { + return "", err + } + y, err := source.FormatNode(binaryExpr.Y) + if err != nil { + return "", err + } + return x + " is " + y, nil + } + + if unaryExpr, ok := expr.(*ast.UnaryExpr); ok && unaryExpr.Op == token.NOT { + x, err := source.FormatNode(unaryExpr.X) + if err != nil { + return "", err + } + return x + " is true", nil + } + + formatted, err := source.FormatNode(expr) + if err != nil { + return "", err + } + return "expression is false: " + formatted, nil +} + +// Assert performs a comparison. If the comparison fails the test is marked as +// failed, a failure message is logged, and execution is stopped immediately. +// +// The comparison argument may be one of three types: bool, cmp.Comparison or +// error. +// When called with a bool the failure message will contain the literal source +// code of the expression. +// When called with a cmp.Comparison the comparison is responsible for producing +// a helpful failure message. +// When called with an error a nil value is considered success. A non-nil error +// is a failure, and Error() is used as the failure message. +func Assert(t TestingT, comparison BoolOrComparison, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + assert(t, t.FailNow, filterExprArgsFromComparison, comparison, msgAndArgs...) +} + +// Check performs a comparison. If the comparison fails the test is marked as +// failed, a failure message is logged, and Check returns false. Otherwise returns +// true. +// +// See Assert for details about the comparison arg and failure messages. +func Check(t TestingT, comparison BoolOrComparison, msgAndArgs ...interface{}) bool { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + return assert(t, t.Fail, filterExprArgsFromComparison, comparison, msgAndArgs...) +} + +// NilError fails the test immediately if err is not nil. +// This is equivalent to Assert(t, err) +func NilError(t TestingT, err error, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + assert(t, t.FailNow, filterExprExcludeFirst, err, msgAndArgs...) +} + +// Equal uses the == operator to assert two values are equal and fails the test +// if they are not equal. This is equivalent to Assert(t, cmp.Equal(x, y)). +func Equal(t TestingT, x, y interface{}, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + assert(t, t.FailNow, filterExprExcludeFirst, cmp.Equal(x, y), msgAndArgs...) +} + +// DeepEqual uses https://github.com/google/go-cmp/cmp to assert two values +// are equal and fails the test if they are not equal. +// This is equivalent to Assert(t, cmp.DeepEqual(x, y)). +func DeepEqual(t TestingT, x, y interface{}, opts ...gocmp.Option) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + assert(t, t.FailNow, filterExprExcludeFirst, cmp.DeepEqual(x, y, opts...)) +} diff --git a/components/cli/vendor/github.com/gotestyourself/gotestyourself/assert/cmp/compare.go b/components/cli/vendor/github.com/gotestyourself/gotestyourself/assert/cmp/compare.go new file mode 100644 index 0000000000..64a25f00b3 --- /dev/null +++ b/components/cli/vendor/github.com/gotestyourself/gotestyourself/assert/cmp/compare.go @@ -0,0 +1,310 @@ +/*Package cmp provides Comparisons for Assert and Check*/ +package cmp + +import ( + "fmt" + "reflect" + "strings" + + "github.com/google/go-cmp/cmp" + "github.com/pmezard/go-difflib/difflib" +) + +// Comparison is a function which compares values and returns ResultSuccess if +// the actual value matches the expected value. If the values do not match the +// Result will contain a message about why it failed. +type Comparison func() Result + +// DeepEqual compares two values using https://godoc.org/github.com/google/go-cmp/cmp +// and succeeds if the values are equal. +// +// The comparison can be customized using comparison Options. +func DeepEqual(x, y interface{}, opts ...cmp.Option) Comparison { + return func() (result Result) { + defer func() { + if panicmsg, handled := handleCmpPanic(recover()); handled { + result = ResultFailure(panicmsg) + } + }() + diff := cmp.Diff(x, y, opts...) + return toResult(diff == "", "\n"+diff) + } +} + +func handleCmpPanic(r interface{}) (string, bool) { + if r == nil { + return "", false + } + panicmsg, ok := r.(string) + if !ok { + panic(r) + } + switch { + case strings.HasPrefix(panicmsg, "cannot handle unexported field"): + return panicmsg, true + } + panic(r) +} + +func toResult(success bool, msg string) Result { + if success { + return ResultSuccess + } + return ResultFailure(msg) +} + +// Equal succeeds if x == y. +func Equal(x, y interface{}) Comparison { + return func() Result { + switch { + case x == y: + return ResultSuccess + case isMultiLineStringCompare(x, y): + return multiLineStringDiffResult(x.(string), y.(string)) + } + return ResultFailureTemplate(` + {{- .Data.x}} ( + {{- with callArg 0 }}{{ formatNode . }} {{end -}} + {{- printf "%T" .Data.x -}} + ) != {{ .Data.y}} ( + {{- with callArg 1 }}{{ formatNode . }} {{end -}} + {{- printf "%T" .Data.y -}} + )`, + map[string]interface{}{"x": x, "y": y}) + } +} + +func isMultiLineStringCompare(x, y interface{}) bool { + strX, ok := x.(string) + if !ok { + return false + } + strY, ok := y.(string) + if !ok { + return false + } + return strings.Contains(strX, "\n") || strings.Contains(strY, "\n") +} + +func multiLineStringDiffResult(x, y string) Result { + diff, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ + A: difflib.SplitLines(x), + B: difflib.SplitLines(y), + Context: 3, + }) + if err != nil { + return ResultFailure(fmt.Sprintf("failed to diff: %s", err)) + } + return ResultFailureTemplate(` +--- {{ with callArg 0 }}{{ formatNode . }}{{else}}←{{end}} ++++ {{ with callArg 1 }}{{ formatNode . }}{{else}}→{{end}} +{{ .Data.diff }}`, + map[string]interface{}{"diff": diff}) +} + +// Len succeeds if the sequence has the expected length. +func Len(seq interface{}, expected int) Comparison { + return func() (result Result) { + defer func() { + if e := recover(); e != nil { + result = ResultFailure(fmt.Sprintf("type %T does not have a length", seq)) + } + }() + value := reflect.ValueOf(seq) + length := value.Len() + if length == expected { + return ResultSuccess + } + msg := fmt.Sprintf("expected %s (length %d) to have length %d", seq, length, expected) + return ResultFailure(msg) + } +} + +// Contains succeeds if item is in collection. Collection may be a string, map, +// slice, or array. +// +// If collection is a string, item must also be a string, and is compared using +// strings.Contains(). +// If collection is a Map, contains will succeed if item is a key in the map. +// If collection is a slice or array, item is compared to each item in the +// sequence using reflect.DeepEqual(). +func Contains(collection interface{}, item interface{}) Comparison { + return func() Result { + colValue := reflect.ValueOf(collection) + if !colValue.IsValid() { + return ResultFailure(fmt.Sprintf("nil does not contain items")) + } + msg := fmt.Sprintf("%v does not contain %v", collection, item) + + itemValue := reflect.ValueOf(item) + switch colValue.Type().Kind() { + case reflect.String: + if itemValue.Type().Kind() != reflect.String { + return ResultFailure("string may only contain strings") + } + return toResult( + strings.Contains(colValue.String(), itemValue.String()), + fmt.Sprintf("string %q does not contain %q", collection, item)) + + case reflect.Map: + if itemValue.Type() != colValue.Type().Key() { + return ResultFailure(fmt.Sprintf( + "%v can not contain a %v key", colValue.Type(), itemValue.Type())) + } + return toResult(colValue.MapIndex(itemValue).IsValid(), msg) + + case reflect.Slice, reflect.Array: + for i := 0; i < colValue.Len(); i++ { + if reflect.DeepEqual(colValue.Index(i).Interface(), item) { + return ResultSuccess + } + } + return ResultFailure(msg) + default: + return ResultFailure(fmt.Sprintf("type %T does not contain items", collection)) + } + } +} + +// Panics succeeds if f() panics. +func Panics(f func()) Comparison { + return func() (result Result) { + defer func() { + if err := recover(); err != nil { + result = ResultSuccess + } + }() + f() + return ResultFailure("did not panic") + } +} + +// Error succeeds if err is a non-nil error, and the error message equals the +// expected message. +func Error(err error, message string) Comparison { + return func() Result { + switch { + case err == nil: + return ResultFailure("expected an error, got nil") + case err.Error() != message: + return ResultFailure(fmt.Sprintf( + "expected error %q, got %+v", message, err)) + } + return ResultSuccess + } +} + +// ErrorContains succeeds if err is a non-nil error, and the error message contains +// the expected substring. +func ErrorContains(err error, substring string) Comparison { + return func() Result { + switch { + case err == nil: + return ResultFailure("expected an error, got nil") + case !strings.Contains(err.Error(), substring): + return ResultFailure(fmt.Sprintf( + "expected error to contain %q, got %+v", substring, err)) + } + return ResultSuccess + } +} + +// Nil succeeds if obj is a nil interface, pointer, or function. +// +// Use NilError() for comparing errors. Use Len(obj, 0) for comparing slices, +// maps, and channels. +func Nil(obj interface{}) Comparison { + msgFunc := func(value reflect.Value) string { + return fmt.Sprintf("%v (type %s) is not nil", reflect.Indirect(value), value.Type()) + } + return isNil(obj, msgFunc) +} + +func isNil(obj interface{}, msgFunc func(reflect.Value) string) Comparison { + return func() Result { + if obj == nil { + return ResultSuccess + } + value := reflect.ValueOf(obj) + kind := value.Type().Kind() + if kind >= reflect.Chan && kind <= reflect.Slice { + if value.IsNil() { + return ResultSuccess + } + return ResultFailure(msgFunc(value)) + } + + return ResultFailure(fmt.Sprintf("%v (type %s) can not be nil", value, value.Type())) + } +} + +// ErrorType succeeds if err is not nil and is of the expected type. +// +// Expected can be one of: +// a func(error) bool which returns true if the error is the expected type, +// an instance of a struct of the expected type, +// a pointer to an interface the error is expected to implement, +// a reflect.Type of the expected struct or interface. +func ErrorType(err error, expected interface{}) Comparison { + return func() Result { + switch expectedType := expected.(type) { + case func(error) bool: + return cmpErrorTypeFunc(err, expectedType) + case reflect.Type: + if expectedType.Kind() == reflect.Interface { + return cmpErrorTypeImplementsType(err, expectedType) + } + return cmpErrorTypeEqualType(err, expectedType) + case nil: + return ResultFailure(fmt.Sprintf("invalid type for expected: nil")) + } + + expectedType := reflect.TypeOf(expected) + switch { + case expectedType.Kind() == reflect.Struct: + return cmpErrorTypeEqualType(err, expectedType) + case isPtrToInterface(expectedType): + return cmpErrorTypeImplementsType(err, expectedType.Elem()) + } + return ResultFailure(fmt.Sprintf("invalid type for expected: %T", expected)) + } +} + +func cmpErrorTypeFunc(err error, f func(error) bool) Result { + if f(err) { + return ResultSuccess + } + actual := "nil" + if err != nil { + actual = fmt.Sprintf("%s (%T)", err, err) + } + return ResultFailureTemplate(`error is {{ .Data.actual }} + {{- with callArg 1 }}, not {{ formatNode . }}{{end -}}`, + map[string]interface{}{"actual": actual}) +} + +func cmpErrorTypeEqualType(err error, expectedType reflect.Type) Result { + if err == nil { + return ResultFailure(fmt.Sprintf("error is nil, not %s", expectedType)) + } + errValue := reflect.ValueOf(err) + if errValue.Type() == expectedType { + return ResultSuccess + } + return ResultFailure(fmt.Sprintf("error is %s (%T), not %s", err, err, expectedType)) +} + +func cmpErrorTypeImplementsType(err error, expectedType reflect.Type) Result { + if err == nil { + return ResultFailure(fmt.Sprintf("error is nil, not %s", expectedType)) + } + errValue := reflect.ValueOf(err) + if errValue.Type().Implements(expectedType) { + return ResultSuccess + } + return ResultFailure(fmt.Sprintf("error is %s (%T), not %s", err, err, expectedType)) +} + +func isPtrToInterface(typ reflect.Type) bool { + return typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Interface +} diff --git a/components/cli/vendor/github.com/gotestyourself/gotestyourself/assert/cmp/result.go b/components/cli/vendor/github.com/gotestyourself/gotestyourself/assert/cmp/result.go new file mode 100644 index 0000000000..99f39eeb27 --- /dev/null +++ b/components/cli/vendor/github.com/gotestyourself/gotestyourself/assert/cmp/result.go @@ -0,0 +1,94 @@ +package cmp + +import ( + "bytes" + "fmt" + "go/ast" + "text/template" + + "github.com/gotestyourself/gotestyourself/internal/source" +) + +// Result of a Comparison. +type Result interface { + Success() bool +} + +type result struct { + success bool + message string +} + +func (r result) Success() bool { + return r.success +} + +func (r result) FailureMessage() string { + return r.message +} + +// ResultSuccess is a constant which is returned by a ComparisonWithResult to +// indicate success. +var ResultSuccess = result{success: true} + +// ResultFailure returns a failed Result with a failure message. +func ResultFailure(message string) Result { + return result{message: message} +} + +// ResultFromError returns ResultSuccess if err is nil. Otherwise ResultFailure +// is returned with the error message as the failure message. +func ResultFromError(err error) Result { + if err == nil { + return ResultSuccess + } + return ResultFailure(err.Error()) +} + +type templatedResult struct { + success bool + template string + data map[string]interface{} +} + +func (r templatedResult) Success() bool { + return r.success +} + +func (r templatedResult) FailureMessage(args []ast.Expr) string { + msg, err := renderMessage(r, args) + if err != nil { + return fmt.Sprintf("failed to render failure message: %s", err) + } + return msg +} + +// ResultFailureTemplate returns a Result with a template string and data which +// can be used to format a failure message. The template may access data from .Data, +// the comparison args with the callArg function, and the formatNode function may +// be used to format the call args. +func ResultFailureTemplate(template string, data map[string]interface{}) Result { + return templatedResult{template: template, data: data} +} + +func renderMessage(result templatedResult, args []ast.Expr) (string, error) { + tmpl := template.New("failure").Funcs(template.FuncMap{ + "formatNode": source.FormatNode, + "callArg": func(index int) ast.Expr { + if index >= len(args) { + return nil + } + return args[index] + }, + }) + var err error + tmpl, err = tmpl.Parse(result.template) + if err != nil { + return "", err + } + buf := new(bytes.Buffer) + err = tmpl.Execute(buf, map[string]interface{}{ + "Data": result.data, + }) + return buf.String(), err +} diff --git a/components/cli/vendor/github.com/gotestyourself/gotestyourself/assert/result.go b/components/cli/vendor/github.com/gotestyourself/gotestyourself/assert/result.go new file mode 100644 index 0000000000..3298bc7552 --- /dev/null +++ b/components/cli/vendor/github.com/gotestyourself/gotestyourself/assert/result.go @@ -0,0 +1,91 @@ +package assert + +import ( + "fmt" + "go/ast" + + "github.com/gotestyourself/gotestyourself/assert/cmp" + "github.com/gotestyourself/gotestyourself/internal/format" + "github.com/gotestyourself/gotestyourself/internal/source" +) + +func runComparison( + t TestingT, + exprFilter astExprListFilter, + f cmp.Comparison, + msgAndArgs ...interface{}, +) bool { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + result := f() + if result.Success() { + return true + } + + var message string + switch typed := result.(type) { + case resultWithComparisonArgs: + const stackIndex = 3 // Assert/Check, assert, runComparison + args, err := source.CallExprArgs(stackIndex) + if err != nil { + t.Log(err.Error()) + } + message = typed.FailureMessage(filterPrintableExpr(exprFilter(args))) + case resultBasic: + message = typed.FailureMessage() + default: + message = fmt.Sprintf("comparison returned invalid Result type: %T", result) + } + + t.Log(format.WithCustomMessage(failureMessage+message, msgAndArgs...)) + return false +} + +type resultWithComparisonArgs interface { + FailureMessage(args []ast.Expr) string +} + +type resultBasic interface { + FailureMessage() string +} + +type astExprListFilter func([]ast.Expr) []ast.Expr + +// filterPrintableExpr filters the ast.Expr slice to only include nodes that are +// easy to read when printed and contain relevant information to an assertion. +// +// Ident and SelectorExpr are included because they print nicely and the variable +// names may provide additional context to their values. +// BasicLit and CompositeLit are excluded because their source is equivalent to +// their value, which is already available. +// Other types are ignored for now, but could be added if they are relevant. +func filterPrintableExpr(args []ast.Expr) []ast.Expr { + result := make([]ast.Expr, len(args)) + for i, arg := range args { + switch arg.(type) { + case *ast.Ident, *ast.SelectorExpr, *ast.IndexExpr, *ast.SliceExpr: + result[i] = arg + default: + result[i] = nil + } + } + return result +} + +func filterExprExcludeFirst(args []ast.Expr) []ast.Expr { + if len(args) < 1 { + return nil + } + return args[1:] +} + +func filterExprArgsFromComparison(args []ast.Expr) []ast.Expr { + if len(args) < 1 { + return nil + } + if callExpr, ok := args[1].(*ast.CallExpr); ok { + return callExpr.Args + } + return nil +} diff --git a/components/cli/vendor/github.com/gotestyourself/gotestyourself/env/env.go b/components/cli/vendor/github.com/gotestyourself/gotestyourself/env/env.go index 61a45a438d..95a53bee9c 100644 --- a/components/cli/vendor/github.com/gotestyourself/gotestyourself/env/env.go +++ b/components/cli/vendor/github.com/gotestyourself/gotestyourself/env/env.go @@ -1,42 +1,59 @@ /*Package env provides functions to test code that read environment variables - */ +or the current working directory. +*/ package env import ( "os" "strings" - "github.com/stretchr/testify/require" + "github.com/gotestyourself/gotestyourself/assert" ) +type helperT interface { + Helper() +} + // Patch changes the value of an environment variable, and returns a // function which will reset the the value of that variable back to the // previous state. -func Patch(t require.TestingT, key, value string) func() { +func Patch(t assert.TestingT, key, value string) func() { + if ht, ok := t.(helperT); ok { + ht.Helper() + } oldValue, ok := os.LookupEnv(key) - require.NoError(t, os.Setenv(key, value)) + assert.NilError(t, os.Setenv(key, value)) return func() { + if ht, ok := t.(helperT); ok { + ht.Helper() + } if !ok { - require.NoError(t, os.Unsetenv(key)) + assert.NilError(t, os.Unsetenv(key)) return } - require.NoError(t, os.Setenv(key, oldValue)) + assert.NilError(t, os.Setenv(key, oldValue)) } } // PatchAll sets the environment to env, and returns a function which will // reset the environment back to the previous state. -func PatchAll(t require.TestingT, env map[string]string) func() { +func PatchAll(t assert.TestingT, env map[string]string) func() { + if ht, ok := t.(helperT); ok { + ht.Helper() + } oldEnv := os.Environ() os.Clearenv() for key, value := range env { - require.NoError(t, os.Setenv(key, value)) + assert.NilError(t, os.Setenv(key, value)) } return func() { + if ht, ok := t.(helperT); ok { + ht.Helper() + } os.Clearenv() for key, oldVal := range ToMap(oldEnv) { - require.NoError(t, os.Setenv(key, oldVal)) + assert.NilError(t, os.Setenv(key, oldVal)) } } } @@ -56,3 +73,20 @@ func ToMap(env []string) map[string]string { } return result } + +// ChangeWorkingDir to the directory, and return a function which restores the +// previous working directory. +func ChangeWorkingDir(t assert.TestingT, dir string) func() { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + cwd, err := os.Getwd() + assert.NilError(t, err) + assert.NilError(t, os.Chdir(dir)) + return func() { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + assert.NilError(t, os.Chdir(cwd)) + } +} diff --git a/components/cli/vendor/github.com/gotestyourself/gotestyourself/fs/file.go b/components/cli/vendor/github.com/gotestyourself/gotestyourself/fs/file.go index dcda10a02a..a2f731093b 100644 --- a/components/cli/vendor/github.com/gotestyourself/gotestyourself/fs/file.go +++ b/components/cli/vendor/github.com/gotestyourself/gotestyourself/fs/file.go @@ -8,29 +8,42 @@ import ( "os" "path/filepath" - "github.com/stretchr/testify/require" + "github.com/gotestyourself/gotestyourself/assert" ) // Path objects return their filesystem path. Both File and Dir implement Path. type Path interface { Path() string + Remove() } +var ( + _ Path = &Dir{} + _ Path = &File{} +) + // File is a temporary file on the filesystem type File struct { path string } +type helperT interface { + Helper() +} + // NewFile creates a new file in a temporary directory using prefix as part of // the filename. The PathOps are applied to the before returning the File. -func NewFile(t require.TestingT, prefix string, ops ...PathOp) *File { +func NewFile(t assert.TestingT, prefix string, ops ...PathOp) *File { + if ht, ok := t.(helperT); ok { + ht.Helper() + } tempfile, err := ioutil.TempFile("", prefix+"-") - require.NoError(t, err) + assert.NilError(t, err) file := &File{path: tempfile.Name()} - require.NoError(t, tempfile.Close()) + assert.NilError(t, tempfile.Close()) for _, op := range ops { - require.NoError(t, op(file)) + assert.NilError(t, op(file)) } return file } @@ -53,13 +66,16 @@ type Dir struct { // NewDir returns a new temporary directory using prefix as part of the directory // name. The PathOps are applied before returning the Dir. -func NewDir(t require.TestingT, prefix string, ops ...PathOp) *Dir { +func NewDir(t assert.TestingT, prefix string, ops ...PathOp) *Dir { + if ht, ok := t.(helperT); ok { + ht.Helper() + } path, err := ioutil.TempDir("", prefix+"-") - require.NoError(t, err) + assert.NilError(t, err) dir := &Dir{path: path} for _, op := range ops { - require.NoError(t, op(dir)) + assert.NilError(t, op(dir)) } return dir } diff --git a/components/cli/vendor/github.com/gotestyourself/gotestyourself/fs/ops.go b/components/cli/vendor/github.com/gotestyourself/gotestyourself/fs/ops.go index 7cc63994c8..bf9d2150b3 100644 --- a/components/cli/vendor/github.com/gotestyourself/gotestyourself/fs/ops.go +++ b/components/cli/vendor/github.com/gotestyourself/gotestyourself/fs/ops.go @@ -4,6 +4,7 @@ import ( "io/ioutil" "os" "path/filepath" + "time" ) // PathOp is a function which accepts a Path to perform some operation @@ -125,3 +126,33 @@ func copyFile(source, dest string) error { } return ioutil.WriteFile(dest, content, 0644) } + +// WithSymlink creates a symlink in the directory which links to target. +// Target must be a path relative to the directory. +// +// Note: the argument order is the inverse of os.Symlink to be consistent with +// the other functions in this package. +func WithSymlink(path, target string) PathOp { + return func(root Path) error { + return os.Symlink(filepath.Join(root.Path(), target), filepath.Join(root.Path(), path)) + } +} + +// WithHardlink creates a link in the directory which links to target. +// Target must be a path relative to the directory. +// +// Note: the argument order is the inverse of os.Link to be consistent with +// the other functions in this package. +func WithHardlink(path, target string) PathOp { + return func(root Path) error { + return os.Link(filepath.Join(root.Path(), target), filepath.Join(root.Path(), path)) + } +} + +// WithTimestamps sets the access and modification times of the file system object +// at path. +func WithTimestamps(atime, mtime time.Time) PathOp { + return func(root Path) error { + return os.Chtimes(root.Path(), atime, mtime) + } +} diff --git a/components/cli/vendor/github.com/gotestyourself/gotestyourself/golden/golden.go b/components/cli/vendor/github.com/gotestyourself/gotestyourself/golden/golden.go index 9b1c8ba4d0..2a036f3852 100644 --- a/components/cli/vendor/github.com/gotestyourself/gotestyourself/golden/golden.go +++ b/components/cli/vendor/github.com/gotestyourself/gotestyourself/golden/golden.go @@ -5,67 +5,132 @@ Golden files are files in the ./testdata/ subdirectory of the package under test package golden import ( + "bytes" "flag" "fmt" "io/ioutil" "path/filepath" + "github.com/gotestyourself/gotestyourself/assert" + "github.com/gotestyourself/gotestyourself/assert/cmp" "github.com/pmezard/go-difflib/difflib" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) var flagUpdate = flag.Bool("test.update-golden", false, "update golden file") -// Get returns the golden file content -func Get(t require.TestingT, filename string) []byte { +type helperT interface { + Helper() +} + +// Get returns the contents of the file in ./testdata +func Get(t assert.TestingT, filename string) []byte { + if ht, ok := t.(helperT); ok { + ht.Helper() + } expected, err := ioutil.ReadFile(Path(filename)) - require.NoError(t, err) + assert.NilError(t, err) return expected } -// Path returns the full path to a golden file +// Path returns the full path to a file in ./testdata func Path(filename string) string { + if filepath.IsAbs(filename) { + return filename + } return filepath.Join("testdata", filename) } -func update(t require.TestingT, filename string, actual []byte) { +func update(filename string, actual []byte) error { if *flagUpdate { - err := ioutil.WriteFile(Path(filename), actual, 0644) - require.NoError(t, err) + return ioutil.WriteFile(Path(filename), actual, 0644) } + return nil } // Assert compares the actual content to the expected content in the golden file. // If the `-test.update-golden` flag is set then the actual content is written // to the golden file. -// Returns whether the assertion was successful (true) or not (false) -func Assert(t require.TestingT, actual string, filename string, msgAndArgs ...interface{}) bool { - expected := Get(t, filename) - update(t, filename, []byte(actual)) - - if assert.ObjectsAreEqual(expected, []byte(actual)) { - return true +// Returns whether the assertion was successful (true) or not (false). +// This is equivalent to assert.Check(t, String(actual, filename)) +// +// Deprecated: In a future version this function will change to use assert.Assert +// instead of assert.Check to be consistent with other assert functions. +// Use assert.Check(t, String(actual, filename) if you want to preserve the +// current behaviour. +func Assert(t assert.TestingT, actual string, filename string, msgAndArgs ...interface{}) bool { + if ht, ok := t.(helperT); ok { + ht.Helper() } + return assert.Check(t, String(actual, filename), msgAndArgs...) +} - diff, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ - A: difflib.SplitLines(string(expected)), - B: difflib.SplitLines(actual), - FromFile: "Expected", - ToFile: "Actual", - Context: 3, - }) - require.NoError(t, err, msgAndArgs...) - return assert.Fail(t, fmt.Sprintf("Not Equal: \n%s", diff), msgAndArgs...) +// String compares actual to the contents of filename and returns success +// if the strings are equal. +func String(actual string, filename string) cmp.Comparison { + return func() cmp.Result { + result, expected := compare([]byte(actual), filename) + if result != nil { + return result + } + diff, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ + A: difflib.SplitLines(string(expected)), + B: difflib.SplitLines(actual), + FromFile: "expected", + ToFile: "actual", + Context: 3, + }) + if err != nil { + return cmp.ResultFromError(err) + } + return cmp.ResultFailure("\n" + diff) + } } // AssertBytes compares the actual result to the expected result in the golden // file. If the `-test.update-golden` flag is set then the actual content is // written to the golden file. // Returns whether the assertion was successful (true) or not (false) -// nolint: lll -func AssertBytes(t require.TestingT, actual []byte, filename string, msgAndArgs ...interface{}) bool { - expected := Get(t, filename) - update(t, filename, actual) - return assert.Equal(t, expected, actual, msgAndArgs...) +// This is equivalent to assert.Check(t, Bytes(actual, filename)) +// +// Deprecated: In a future version this function will change to use assert.Assert +// instead of assert.Check to be consistent with other assert functions. +// Use assert.Check(t, Bytes(actual, filename) if you want to preserve the +// current behaviour. +func AssertBytes( + t assert.TestingT, + actual []byte, + filename string, + msgAndArgs ...interface{}, +) bool { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + return assert.Check(t, Bytes(actual, filename), msgAndArgs...) +} + +// Bytes compares actual to the contents of filename and returns success +// if the bytes are equal. +func Bytes(actual []byte, filename string) cmp.Comparison { + return func() cmp.Result { + result, expected := compare(actual, filename) + if result != nil { + return result + } + msg := fmt.Sprintf("%v (actual) != %v (expected)", actual, expected) + return cmp.ResultFailure(msg) + } +} + +func compare(actual []byte, filename string) (cmp.Result, []byte) { + if err := update(filename, actual); err != nil { + return cmp.ResultFromError(err), nil + } + expected, err := ioutil.ReadFile(Path(filename)) + if err != nil { + return cmp.ResultFromError(err), nil + } + if bytes.Equal(expected, actual) { + return cmp.ResultSuccess, nil + } + return nil, expected } diff --git a/components/cli/vendor/github.com/gotestyourself/gotestyourself/icmd/command.go b/components/cli/vendor/github.com/gotestyourself/gotestyourself/icmd/command.go index 8729457b4f..0066c51514 100644 --- a/components/cli/vendor/github.com/gotestyourself/gotestyourself/icmd/command.go +++ b/components/cli/vendor/github.com/gotestyourself/gotestyourself/icmd/command.go @@ -7,19 +7,20 @@ import ( "fmt" "io" "os/exec" - "path/filepath" - "runtime" "strings" "sync" "time" + + "github.com/gotestyourself/gotestyourself/assert" + "github.com/gotestyourself/gotestyourself/assert/cmp" ) -type testingT interface { - Fatalf(string, ...interface{}) +type helperT interface { + Helper() } // None is a token to inform Result.Assert that the output should be empty -const None string = "[NOTHING]" +const None = "[NOTHING]" type lockedBuffer struct { m sync.RWMutex @@ -51,24 +52,32 @@ type Result struct { // Assert compares the Result against the Expected struct, and fails the test if // any of the expectations are not met. -func (r *Result) Assert(t testingT, exp Expected) *Result { - err := r.Compare(exp) - if err == nil { - return r +// +// This function is equivalent to assert.Assert(t, result.Equal(exp)). +func (r *Result) Assert(t assert.TestingT, exp Expected) *Result { + if ht, ok := t.(helperT); ok { + ht.Helper() } - _, file, line, ok := runtime.Caller(1) - if ok { - t.Fatalf("at %s:%d - %s\n", filepath.Base(file), line, err.Error()) - } else { - t.Fatalf("(no file/line info) - %s", err.Error()) - } - return nil + assert.Assert(t, r.Equal(exp)) + return r } -// Compare returns a formatted error with the command, stdout, stderr, exit -// code, and any failed expectations -// nolint: gocyclo +// Equal compares the result to Expected. If the result doesn't match expected +// returns a formatted failure message with the command, stdout, stderr, exit code, +// and any failed expectations. +func (r *Result) Equal(exp Expected) cmp.Comparison { + return func() cmp.Result { + return cmp.ResultFromError(r.match(exp)) + } +} + +// Compare the result to Expected and return an error if they do not match. func (r *Result) Compare(exp Expected) error { + return r.match(exp) +} + +// nolint: gocyclo +func (r *Result) match(exp Expected) error { errors := []string{} add := func(format string, args ...interface{}) { errors = append(errors, fmt.Sprintf(format, args...)) diff --git a/components/cli/vendor/github.com/gotestyourself/gotestyourself/internal/format/format.go b/components/cli/vendor/github.com/gotestyourself/gotestyourself/internal/format/format.go new file mode 100644 index 0000000000..6961ffe6da --- /dev/null +++ b/components/cli/vendor/github.com/gotestyourself/gotestyourself/internal/format/format.go @@ -0,0 +1,27 @@ +package format + +import "fmt" + +// Message accepts a msgAndArgs varargs and formats it using fmt.Sprintf +func Message(msgAndArgs ...interface{}) string { + switch len(msgAndArgs) { + case 0: + return "" + case 1: + return fmt.Sprintf("%v", msgAndArgs[0]) + default: + return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) + } +} + +// WithCustomMessage accepts one or two messages and formats them appropriately +func WithCustomMessage(source string, msgAndArgs ...interface{}) string { + custom := Message(msgAndArgs...) + switch { + case custom == "": + return source + case source == "": + return custom + } + return fmt.Sprintf("%s: %s", source, custom) +} diff --git a/components/cli/vendor/github.com/gotestyourself/gotestyourself/internal/source/source.go b/components/cli/vendor/github.com/gotestyourself/gotestyourself/internal/source/source.go new file mode 100644 index 0000000000..f71c5129b8 --- /dev/null +++ b/components/cli/vendor/github.com/gotestyourself/gotestyourself/internal/source/source.go @@ -0,0 +1,163 @@ +package source + +import ( + "bytes" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "os" + "runtime" + "strconv" + "strings" + + "github.com/pkg/errors" +) + +const baseStackIndex = 1 + +// FormattedCallExprArg returns the argument from an ast.CallExpr at the +// index in the call stack. The argument is formatted using FormatNode. +func FormattedCallExprArg(stackIndex int, argPos int) (string, error) { + args, err := CallExprArgs(stackIndex + 1) + if err != nil { + return "", err + } + return FormatNode(args[argPos]) +} + +func getNodeAtLine(filename string, lineNum int) (ast.Node, error) { + fileset := token.NewFileSet() + astFile, err := parser.ParseFile(fileset, filename, nil, parser.AllErrors) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse source file: %s", filename) + } + + node := scanToLine(fileset, astFile, lineNum) + if node == nil { + return nil, errors.Errorf( + "failed to find an expression on line %d in %s", lineNum, filename) + } + return node, nil +} + +func scanToLine(fileset *token.FileSet, node ast.Node, lineNum int) ast.Node { + v := &scanToLineVisitor{lineNum: lineNum, fileset: fileset} + ast.Walk(v, node) + return v.matchedNode +} + +type scanToLineVisitor struct { + lineNum int + matchedNode ast.Node + fileset *token.FileSet +} + +func (v *scanToLineVisitor) Visit(node ast.Node) ast.Visitor { + if node == nil || v.matchedNode != nil { + return nil + } + if v.nodePosition(node).Line == v.lineNum { + v.matchedNode = node + return nil + } + return v +} + +// In golang 1.9 the line number changed from being the line where the statement +// ended to the line where the statement began. +func (v *scanToLineVisitor) nodePosition(node ast.Node) token.Position { + if goVersionBefore19 { + return v.fileset.Position(node.End()) + } + return v.fileset.Position(node.Pos()) +} + +var goVersionBefore19 = isGOVersionBefore19() + +func isGOVersionBefore19() bool { + version := runtime.Version() + // not a release version + if !strings.HasPrefix(version, "go") { + return false + } + version = strings.TrimPrefix(version, "go") + parts := strings.Split(version, ".") + if len(parts) < 2 { + return false + } + minor, err := strconv.ParseInt(parts[1], 10, 32) + return err == nil && parts[0] == "1" && minor < 9 +} + +func getCallExprArgs(node ast.Node) ([]ast.Expr, error) { + visitor := &callExprVisitor{} + ast.Walk(visitor, node) + if visitor.expr == nil { + return nil, errors.New("failed to find call expression") + } + return visitor.expr.Args, nil +} + +type callExprVisitor struct { + expr *ast.CallExpr +} + +func (v *callExprVisitor) Visit(node ast.Node) ast.Visitor { + if v.expr != nil || node == nil { + return nil + } + debug("visit (%T): %s", node, debugFormatNode{node}) + + if callExpr, ok := node.(*ast.CallExpr); ok { + v.expr = callExpr + return nil + } + return v +} + +// FormatNode using go/format.Node and return the result as a string +func FormatNode(node ast.Node) (string, error) { + buf := new(bytes.Buffer) + err := format.Node(buf, token.NewFileSet(), node) + return buf.String(), err +} + +// CallExprArgs returns the ast.Expr slice for the args of an ast.CallExpr at +// the index in the call stack. +func CallExprArgs(stackIndex int) ([]ast.Expr, error) { + _, filename, lineNum, ok := runtime.Caller(baseStackIndex + stackIndex) + if !ok { + return nil, errors.New("failed to get call stack") + } + debug("call stack position: %s:%d", filename, lineNum) + + node, err := getNodeAtLine(filename, lineNum) + if err != nil { + return nil, err + } + debug("found node (%T): %s", node, debugFormatNode{node}) + + return getCallExprArgs(node) +} + +var debugEnabled = os.Getenv("GOTESTYOURSELF_DEBUG") != "" + +func debug(format string, args ...interface{}) { + if debugEnabled { + fmt.Fprintf(os.Stderr, "DEBUG: "+format+"\n", args...) + } +} + +type debugFormatNode struct { + ast.Node +} + +func (n debugFormatNode) String() string { + out, err := FormatNode(n.Node) + if err != nil { + return fmt.Sprintf("failed to format %s: %s", n.Node, err) + } + return out +} diff --git a/components/cli/vendor/github.com/gotestyourself/gotestyourself/poll/poll.go b/components/cli/vendor/github.com/gotestyourself/gotestyourself/poll/poll.go index 3838d38e13..42a42cc149 100644 --- a/components/cli/vendor/github.com/gotestyourself/gotestyourself/poll/poll.go +++ b/components/cli/vendor/github.com/gotestyourself/gotestyourself/poll/poll.go @@ -19,6 +19,10 @@ type LogT interface { Logf(format string, args ...interface{}) } +type helperT interface { + Helper() +} + // Settings are used to configure the behaviour of WaitOn type Settings struct { // Timeout is the maximum time to wait for the condition. Defaults to 10s @@ -101,6 +105,9 @@ func Error(err error) Result { // check returns a done Result. To fail a test and exit polling with an error // return a error result. func WaitOn(t TestingT, check func(t LogT) Result, pollOps ...SettingOp) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } config := defaultConfig() for _, pollOp := range pollOps { pollOp(config) diff --git a/components/cli/vendor/github.com/gotestyourself/gotestyourself/skip/skip.go b/components/cli/vendor/github.com/gotestyourself/gotestyourself/skip/skip.go index 1a6446593f..46ab288819 100644 --- a/components/cli/vendor/github.com/gotestyourself/gotestyourself/skip/skip.go +++ b/components/cli/vendor/github.com/gotestyourself/gotestyourself/skip/skip.go @@ -3,19 +3,14 @@ package skip import ( - "bytes" "fmt" - "go/ast" - "go/format" - "go/parser" - "go/token" - "io/ioutil" "path" "reflect" "runtime" "strings" - "github.com/pkg/errors" + "github.com/gotestyourself/gotestyourself/internal/format" + "github.com/gotestyourself/gotestyourself/internal/source" ) type skipT interface { @@ -23,14 +18,29 @@ type skipT interface { Log(args ...interface{}) } +type helperT interface { + Helper() +} + +// BoolOrCheckFunc can be a bool or func() bool, other types will panic +type BoolOrCheckFunc interface{} + // If skips the test if the check function returns true. The skip message will // contain the name of the check function. Extra message text can be passed as a // format string with args -func If(t skipT, check func() bool, msgAndArgs ...interface{}) { - if check() { - t.Skip(formatWithCustomMessage( - getFunctionName(check), - formatMessage(msgAndArgs...))) +func If(t skipT, condition BoolOrCheckFunc, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + switch check := condition.(type) { + case bool: + ifCondition(t, check, msgAndArgs...) + case func() bool: + if check() { + t.Skip(format.WithCustomMessage(getFunctionName(check), msgAndArgs...)) + } + default: + panic(fmt.Sprintf("invalid type for condition arg: %T", check)) } } @@ -42,91 +52,30 @@ func getFunctionName(function func() bool) string { // IfCondition skips the test if the condition is true. The skip message will // contain the source of the expression passed as the condition. Extra message // text can be passed as a format string with args. +// +// Deprecated: Use If() which now accepts bool arguments func IfCondition(t skipT, condition bool, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + ifCondition(t, condition, msgAndArgs...) +} + +func ifCondition(t skipT, condition bool, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } if !condition { return } - source, err := getConditionSource() + const ( + stackIndex = 2 + argPos = 1 + ) + source, err := source.FormattedCallExprArg(stackIndex, argPos) if err != nil { t.Log(err.Error()) - t.Skip(formatMessage(msgAndArgs...)) + t.Skip(format.Message(msgAndArgs...)) } - t.Skip(formatWithCustomMessage(source, formatMessage(msgAndArgs...))) -} - -// getConditionSource returns the condition string by reading it from the file -// identified in the callstack. In golang 1.9 the line number changed from -// being the line where the statement ended to the line where the statement began. -func getConditionSource() (string, error) { - lines, err := getSourceLine() - if err != nil { - return "", err - } - - for i := range lines { - node, err := parser.ParseExpr(getSource(lines, i)) - if err == nil { - return getConditionArgFromAST(node) - } - } - return "", errors.Wrapf(err, "failed to parse source") -} - -// maxContextLines is the maximum number of lines to scan for a complete -// skip.If() statement -const maxContextLines = 10 - -// getSourceLines returns the source line which called skip.If() along with a -// few preceding lines. To properly parse the AST a complete statement is -// required, and that statement may be split across multiple lines, so include -// up to maxContextLines. -func getSourceLine() ([]string, error) { - const stackIndex = 3 - _, filename, line, ok := runtime.Caller(stackIndex) - if !ok { - return nil, errors.New("failed to get caller info") - } - - raw, err := ioutil.ReadFile(filename) - if err != nil { - return nil, errors.Wrapf(err, "failed to read source file: %s", filename) - } - - lines := strings.Split(string(raw), "\n") - if len(lines) < line { - return nil, errors.Errorf("file %s does not have line %d", filename, line) - } - firstLine, lastLine := getSourceLinesRange(line, len(lines)) - return lines[firstLine:lastLine], nil -} - -func getConditionArgFromAST(node ast.Expr) (string, error) { - switch expr := node.(type) { - case *ast.CallExpr: - buf := new(bytes.Buffer) - err := format.Node(buf, token.NewFileSet(), expr.Args[1]) - return buf.String(), err - } - return "", errors.New("unexpected ast") -} - -func formatMessage(msgAndArgs ...interface{}) string { - switch len(msgAndArgs) { - case 0: - return "" - case 1: - return msgAndArgs[0].(string) - default: - return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) - } -} - -func formatWithCustomMessage(source, custom string) string { - switch { - case custom == "": - return source - case source == "": - return custom - } - return fmt.Sprintf("%s: %s", source, custom) + t.Skip(format.WithCustomMessage(source, msgAndArgs...)) } diff --git a/components/cli/vendor/github.com/gotestyourself/gotestyourself/skip/skip_go18.go b/components/cli/vendor/github.com/gotestyourself/gotestyourself/skip/skip_go18.go deleted file mode 100644 index 586e58a735..0000000000 --- a/components/cli/vendor/github.com/gotestyourself/gotestyourself/skip/skip_go18.go +++ /dev/null @@ -1,17 +0,0 @@ -// +build !go1.9,!go.10,!go.11,!go1.12 - -package skip - -import "strings" - -func getSourceLinesRange(line int, _ int) (int, int) { - firstLine := line - maxContextLines - if firstLine < 0 { - firstLine = 0 - } - return firstLine, line -} - -func getSource(lines []string, i int) string { - return strings.Join(lines[len(lines)-i-1:], "\n") -} diff --git a/components/cli/vendor/github.com/gotestyourself/gotestyourself/skip/skip_go19.go b/components/cli/vendor/github.com/gotestyourself/gotestyourself/skip/skip_go19.go deleted file mode 100644 index 8aedc4b2c1..0000000000 --- a/components/cli/vendor/github.com/gotestyourself/gotestyourself/skip/skip_go19.go +++ /dev/null @@ -1,17 +0,0 @@ -// +build go1.9 - -package skip - -import "strings" - -func getSourceLinesRange(line int, lines int) (int, int) { - lastLine := line + maxContextLines - if lastLine > lines { - lastLine = lines - } - return line - 1, lastLine -} - -func getSource(lines []string, i int) string { - return strings.Join(lines[:i], "\n") -} From e11e04d02bc5ea49c85e6f534b7127e94ea24193 Mon Sep 17 00:00:00 2001 From: Kevin Kirsche Date: Wed, 28 Feb 2018 11:24:19 -0500 Subject: [PATCH 13/13] Fix grammar typo in comment Overriding is the incorrect part of speech for this sentence. It is more common to state that the values are overridden instead. Other options would include: ```golang // ldflags is overriding these values ``` ```golang // These values will be overridden by ldflags ``` etc. Signed-off-by: Kevin Kirsche Upstream-commit: 0e2fdc92ce45d893a0ffb807668574f1575d6c92 Component: cli --- components/cli/cli/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cli/version.go b/components/cli/cli/version.go index 91c9ae1878..c4120b9585 100644 --- a/components/cli/cli/version.go +++ b/components/cli/cli/version.go @@ -1,7 +1,7 @@ package cli // Default build-time variable. -// These values are overriding via ldflags +// These values are overridden via ldflags var ( PlatformName = "" Version = "unknown-version"