<commit_before>package help

import (
	"fmt"
	"os"
	"strings"
	"text/tabwriter"
	"text/template"
	"time"
	"unicode/utf8"

	"github.com/cloudfoundry/cli/cf"
	"github.com/cloudfoundry/cli/cf/command_registry"
	"github.com/cloudfoundry/cli/cf/configuration/plugin_config"
	. "github.com/cloudfoundry/cli/cf/i18n"
	"github.com/cloudfoundry/cli/cf/terminal"
)

type appPresenter struct {
	Name     string
	Usage    string
	Version  string
	Compiled time.Time
	Commands []groupedCommands
}

type groupedCommands struct {
	Name             string
	CommandSubGroups [][]cmdPresenter
}

type cmdPresenter struct {
	Name        string
	Description string
}

func ShowHelp(helpTemplate string) {
	translatedTemplatedHelp := T(strings.Replace(helpTemplate, "{{", "[[", -1))
	translatedTemplatedHelp = strings.Replace(translatedTemplatedHelp, "[[", "{{", -1)

	showAppHelp(translatedTemplatedHelp)
}

func showAppHelp(helpTemplate string) {
	presenter := newAppPresenter()

	w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
	t := template.Must(template.New("help").Parse(helpTemplate))
	err := t.Execute(w, presenter)
	if err != nil {
		fmt.Println("error", err)
	}
	w.Flush()
}

func newAppPresenter() (presenter appPresenter) {
	maxNameLen := command_registry.Commands.MaxCommandNameLength()

	presentNonCodegangstaCommand := func(commandName string) (presenter cmdPresenter) {
		cmd := command_registry.Commands.FindCommand(commandName)
		presenter.Name = cmd.MetaData().Name
		padding := strings.Repeat(" ", maxNameLen-utf8.RuneCountInString(presenter.Name))
		presenter.Name = presenter.Name + padding
		presenter.Description = cmd.MetaData().Description
		return
	}

	presentPluginCommands := func() []cmdPresenter {
		pluginConfig := plugin_config.NewPluginConfig(func(err error) {
			//fail silently when running help?
		})

		plugins := pluginConfig.Plugins()
		var presenters []cmdPresenter
		var pluginPresenter cmdPresenter

		for _, pluginMetadata := range plugins {
			for _, cmd := range pluginMetadata.Commands {

				if cmd.Alias == "" {
					pluginPresenter.Name = cmd.Name
				} else {
					pluginPresenter.Name = cmd.Name + ", " + cmd.Alias
				}

				padding := strings.Repeat(" ", maxNameLen-utf8.RuneCountInString(pluginPresenter.Name))
				pluginPresenter.Name = pluginPresenter.Name + padding
				pluginPresenter.Description = cmd.HelpText
				presenters = append(presenters, pluginPresenter)
			}
		}

		return presenters
	}

	presenter.Name = os.Args[0]
	presenter.Usage = T("A command line tool to interact with Cloud Foundry")
	presenter.Version = cf.Version + "-" + cf.BuiltOnDate
	compiledAtTime, err := time.Parse("2006-01-02T03:04:05+00:00", cf.BuiltOnDate)
	if err == nil {
		presenter.Compiled = compiledAtTime
	} else {
		presenter.Compiled = time.Now()
	}
	presenter.Commands = []groupedCommands{
		{
			Name: T("GETTING STARTED"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("help"),
					presentNonCodegangstaCommand("login"),
					presentNonCodegangstaCommand("logout"),
					presentNonCodegangstaCommand("passwd"),
					presentNonCodegangstaCommand("target"),
				}, {
					presentNonCodegangstaCommand("api"),
					presentNonCodegangstaCommand("auth"),
				},
			},
		}, {
			Name: T("APPS"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("apps"),
					presentNonCodegangstaCommand("app"),
				}, {
					presentNonCodegangstaCommand("push"),
					presentNonCodegangstaCommand("scale"),
					presentNonCodegangstaCommand("delete"),
					presentNonCodegangstaCommand("rename"),
				}, {
					presentNonCodegangstaCommand("start"),
					presentNonCodegangstaCommand("stop"),
					presentNonCodegangstaCommand("restart"),
					presentNonCodegangstaCommand("restage"),
					presentNonCodegangstaCommand("restart-app-instance"),
				}, {
					presentNonCodegangstaCommand("events"),
					presentNonCodegangstaCommand("files"),
					presentNonCodegangstaCommand("logs"),
				}, {
					presentNonCodegangstaCommand("env"),
					presentNonCodegangstaCommand("set-env"),
					presentNonCodegangstaCommand("unset-env"),
				}, {
					presentNonCodegangstaCommand("stacks"),
					presentNonCodegangstaCommand("stack"),
				}, {
					presentNonCodegangstaCommand("copy-source"),
				}, {
					presentNonCodegangstaCommand("create-app-manifest"),
				}, {
					presentNonCodegangstaCommand("get-health-check"),
					presentNonCodegangstaCommand("set-health-check"),
					presentNonCodegangstaCommand("enable-ssh"),
					presentNonCodegangstaCommand("disable-ssh"),
					presentNonCodegangstaCommand("ssh-enabled"),
				},
			},
		}, {
			Name: T("SERVICES"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("marketplace"),
					presentNonCodegangstaCommand("services"),
					presentNonCodegangstaCommand("service"),
				}, {
					presentNonCodegangstaCommand("create-service"),
					presentNonCodegangstaCommand("update-service"),
					presentNonCodegangstaCommand("delete-service"),
					presentNonCodegangstaCommand("rename-service"),
				}, {
					presentNonCodegangstaCommand("create-service-key"),
					presentNonCodegangstaCommand("service-keys"),
					presentNonCodegangstaCommand("service-key"),
					presentNonCodegangstaCommand("delete-service-key"),
				}, {
					presentNonCodegangstaCommand("bind-service"),
					presentNonCodegangstaCommand("unbind-service"),
				}, {
					presentNonCodegangstaCommand("create-user-provided-service"),
					presentNonCodegangstaCommand("update-user-provided-service"),
				},
			},
		}, {
			Name: T("ORGS"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("orgs"),
					presentNonCodegangstaCommand("org"),
				}, {
					presentNonCodegangstaCommand("create-org"),
					presentNonCodegangstaCommand("delete-org"),
					presentNonCodegangstaCommand("rename-org"),
				},
			},
		}, {
			Name: T("SPACES"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("spaces"),
					presentNonCodegangstaCommand("space"),
				}, {
					presentNonCodegangstaCommand("create-space"),
					presentNonCodegangstaCommand("delete-space"),
					presentNonCodegangstaCommand("rename-space"),
				}, {
					presentNonCodegangstaCommand("allow-space-ssh"),
					presentNonCodegangstaCommand("disallow-space-ssh"),
					presentNonCodegangstaCommand("space-ssh-allowed"),
				},
			},
		}, {
			Name: T("DOMAINS"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("domains"),
					presentNonCodegangstaCommand("create-domain"),
					presentNonCodegangstaCommand("delete-domain"),
					presentNonCodegangstaCommand("create-shared-domain"),
					presentNonCodegangstaCommand("delete-shared-domain"),
				},
			},
		}, {
			Name: T("ROUTES"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("routes"),
					presentNonCodegangstaCommand("create-route"),
					presentNonCodegangstaCommand("check-route"),
					presentNonCodegangstaCommand("map-route"),
					presentNonCodegangstaCommand("unmap-route"),
					presentNonCodegangstaCommand("delete-route"),
					presentNonCodegangstaCommand("delete-orphaned-routes"),
				},
			},
		}, {
			Name: T("BUILDPACKS"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("buildpacks"),
					presentNonCodegangstaCommand("create-buildpack"),
					presentNonCodegangstaCommand("update-buildpack"),
					presentNonCodegangstaCommand("rename-buildpack"),
					presentNonCodegangstaCommand("delete-buildpack"),
				},
			},
		}, {
			Name: T("USER ADMIN"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("create-user"),
					presentNonCodegangstaCommand("delete-user"),
				}, {
					presentNonCodegangstaCommand("org-users"),
					presentNonCodegangstaCommand("set-org-role"),
					presentNonCodegangstaCommand("unset-org-role"),
				}, {
					presentNonCodegangstaCommand("space-users"),
					presentNonCodegangstaCommand("set-space-role"),
					presentNonCodegangstaCommand("unset-space-role"),
				},
			},
		}, {
			Name: T("ORG ADMIN"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("quotas"),
					presentNonCodegangstaCommand("quota"),
					presentNonCodegangstaCommand("set-quota"),
				}, {
					presentNonCodegangstaCommand("create-quota"),
					presentNonCodegangstaCommand("delete-quota"),
					presentNonCodegangstaCommand("update-quota"),
				},
				{
					presentNonCodegangstaCommand("share-private-domain"),
					presentNonCodegangstaCommand("unshare-private-domain"),
				},
			},
		}, {
			Name: T("SPACE ADMIN"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("space-quotas"),
					presentNonCodegangstaCommand("space-quota"),
					presentNonCodegangstaCommand("create-space-quota"),
					presentNonCodegangstaCommand("update-space-quota"),
					presentNonCodegangstaCommand("delete-space-quota"),
					presentNonCodegangstaCommand("set-space-quota"),
					presentNonCodegangstaCommand("unset-space-quota"),
				},
			},
		}, {
			Name: T("SERVICE ADMIN"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("service-auth-tokens"),
					presentNonCodegangstaCommand("create-service-auth-token"),
					presentNonCodegangstaCommand("update-service-auth-token"),
					presentNonCodegangstaCommand("delete-service-auth-token"),
				}, {
					presentNonCodegangstaCommand("service-brokers"),
					presentNonCodegangstaCommand("create-service-broker"),
					presentNonCodegangstaCommand("update-service-broker"),
					presentNonCodegangstaCommand("delete-service-broker"),
					presentNonCodegangstaCommand("rename-service-broker"),
				}, {
					presentNonCodegangstaCommand("migrate-service-instances"),
					presentNonCodegangstaCommand("purge-service-offering"),
				}, {
					presentNonCodegangstaCommand("service-access"),
					presentNonCodegangstaCommand("enable-service-access"),
					presentNonCodegangstaCommand("disable-service-access"),
				},
			},
		}, {
			Name: T("SECURITY GROUP"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("security-group"),
					presentNonCodegangstaCommand("security-groups"),
					presentNonCodegangstaCommand("create-security-group"),
					presentNonCodegangstaCommand("update-security-group"),
					presentNonCodegangstaCommand("delete-security-group"),
					presentNonCodegangstaCommand("bind-security-group"),
					presentNonCodegangstaCommand("unbind-security-group"),
				}, {
					presentNonCodegangstaCommand("bind-staging-security-group"),
					presentNonCodegangstaCommand("staging-security-groups"),
					presentNonCodegangstaCommand("unbind-staging-security-group"),
				}, {
					presentNonCodegangstaCommand("bind-running-security-group"),
					presentNonCodegangstaCommand("running-security-groups"),
					presentNonCodegangstaCommand("unbind-running-security-group"),
				},
			},
		}, {
			Name: T("ENVIRONMENT VARIABLE GROUPS"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("running-environment-variable-group"),
					presentNonCodegangstaCommand("staging-environment-variable-group"),
					presentNonCodegangstaCommand("set-staging-environment-variable-group"),
					presentNonCodegangstaCommand("set-running-environment-variable-group"),
				},
			},
		},
		{
			Name: T("FEATURE FLAGS"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("feature-flags"),
					presentNonCodegangstaCommand("feature-flag"),
					presentNonCodegangstaCommand("enable-feature-flag"),
					presentNonCodegangstaCommand("disable-feature-flag"),
				},
			},
		}, {
			Name: T("ADVANCED"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("curl"),
					presentNonCodegangstaCommand("config"),
					presentNonCodegangstaCommand("oauth-token"),
				},
			},
		}, {
			Name: T("ADD/REMOVE PLUGIN REPOSITORY"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("add-plugin-repo"),
					presentNonCodegangstaCommand("remove-plugin-repo"),
					presentNonCodegangstaCommand("list-plugin-repos"),
					presentNonCodegangstaCommand("repo-plugins"),
				},
			},
		}, {
			Name: T("ADD/REMOVE PLUGIN"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("plugins"),
					presentNonCodegangstaCommand("install-plugin"),
					presentNonCodegangstaCommand("uninstall-plugin"),
				},
			},
		}, {
			Name: T("INSTALLED PLUGIN COMMANDS"),
			CommandSubGroups: [][]cmdPresenter{
				presentPluginCommands(),
			},
		},
	}

	return
}

func (p appPresenter) Title(name string) string {
	return terminal.HeaderColor(name)
}

func (c groupedCommands) SubTitle(name string) string {
	return terminal.HeaderColor(name + ":")
}
<commit_msg>add command ssh to cf help<commit_after>package help

import (
	"fmt"
	"os"
	"strings"
	"text/tabwriter"
	"text/template"
	"time"
	"unicode/utf8"

	"github.com/cloudfoundry/cli/cf"
	"github.com/cloudfoundry/cli/cf/command_registry"
	"github.com/cloudfoundry/cli/cf/configuration/plugin_config"
	. "github.com/cloudfoundry/cli/cf/i18n"
	"github.com/cloudfoundry/cli/cf/terminal"
)

type appPresenter struct {
	Name     string
	Usage    string
	Version  string
	Compiled time.Time
	Commands []groupedCommands
}

type groupedCommands struct {
	Name             string
	CommandSubGroups [][]cmdPresenter
}

type cmdPresenter struct {
	Name        string
	Description string
}

func ShowHelp(helpTemplate string) {
	translatedTemplatedHelp := T(strings.Replace(helpTemplate, "{{", "[[", -1))
	translatedTemplatedHelp = strings.Replace(translatedTemplatedHelp, "[[", "{{", -1)

	showAppHelp(translatedTemplatedHelp)
}

func showAppHelp(helpTemplate string) {
	presenter := newAppPresenter()

	w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
	t := template.Must(template.New("help").Parse(helpTemplate))
	err := t.Execute(w, presenter)
	if err != nil {
		fmt.Println("error", err)
	}
	w.Flush()
}

func newAppPresenter() (presenter appPresenter) {
	maxNameLen := command_registry.Commands.MaxCommandNameLength()

	presentNonCodegangstaCommand := func(commandName string) (presenter cmdPresenter) {
		cmd := command_registry.Commands.FindCommand(commandName)
		presenter.Name = cmd.MetaData().Name
		padding := strings.Repeat(" ", maxNameLen-utf8.RuneCountInString(presenter.Name))
		presenter.Name = presenter.Name + padding
		presenter.Description = cmd.MetaData().Description
		return
	}

	presentPluginCommands := func() []cmdPresenter {
		pluginConfig := plugin_config.NewPluginConfig(func(err error) {
			//fail silently when running help?
		})

		plugins := pluginConfig.Plugins()
		var presenters []cmdPresenter
		var pluginPresenter cmdPresenter

		for _, pluginMetadata := range plugins {
			for _, cmd := range pluginMetadata.Commands {

				if cmd.Alias == "" {
					pluginPresenter.Name = cmd.Name
				} else {
					pluginPresenter.Name = cmd.Name + ", " + cmd.Alias
				}

				padding := strings.Repeat(" ", maxNameLen-utf8.RuneCountInString(pluginPresenter.Name))
				pluginPresenter.Name = pluginPresenter.Name + padding
				pluginPresenter.Description = cmd.HelpText
				presenters = append(presenters, pluginPresenter)
			}
		}

		return presenters
	}

	presenter.Name = os.Args[0]
	presenter.Usage = T("A command line tool to interact with Cloud Foundry")
	presenter.Version = cf.Version + "-" + cf.BuiltOnDate
	compiledAtTime, err := time.Parse("2006-01-02T03:04:05+00:00", cf.BuiltOnDate)
	if err == nil {
		presenter.Compiled = compiledAtTime
	} else {
		presenter.Compiled = time.Now()
	}
	presenter.Commands = []groupedCommands{
		{
			Name: T("GETTING STARTED"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("help"),
					presentNonCodegangstaCommand("login"),
					presentNonCodegangstaCommand("logout"),
					presentNonCodegangstaCommand("passwd"),
					presentNonCodegangstaCommand("target"),
				}, {
					presentNonCodegangstaCommand("api"),
					presentNonCodegangstaCommand("auth"),
				},
			},
		}, {
			Name: T("APPS"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("apps"),
					presentNonCodegangstaCommand("app"),
				}, {
					presentNonCodegangstaCommand("push"),
					presentNonCodegangstaCommand("scale"),
					presentNonCodegangstaCommand("delete"),
					presentNonCodegangstaCommand("rename"),
				}, {
					presentNonCodegangstaCommand("start"),
					presentNonCodegangstaCommand("stop"),
					presentNonCodegangstaCommand("restart"),
					presentNonCodegangstaCommand("restage"),
					presentNonCodegangstaCommand("restart-app-instance"),
				}, {
					presentNonCodegangstaCommand("events"),
					presentNonCodegangstaCommand("files"),
					presentNonCodegangstaCommand("logs"),
				}, {
					presentNonCodegangstaCommand("env"),
					presentNonCodegangstaCommand("set-env"),
					presentNonCodegangstaCommand("unset-env"),
				}, {
					presentNonCodegangstaCommand("stacks"),
					presentNonCodegangstaCommand("stack"),
				}, {
					presentNonCodegangstaCommand("copy-source"),
				}, {
					presentNonCodegangstaCommand("create-app-manifest"),
				}, {
					presentNonCodegangstaCommand("get-health-check"),
					presentNonCodegangstaCommand("set-health-check"),
					presentNonCodegangstaCommand("enable-ssh"),
					presentNonCodegangstaCommand("disable-ssh"),
					presentNonCodegangstaCommand("ssh-enabled"),
					presentNonCodegangstaCommand("ssh"),
				},
			},
		}, {
			Name: T("SERVICES"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("marketplace"),
					presentNonCodegangstaCommand("services"),
					presentNonCodegangstaCommand("service"),
				}, {
					presentNonCodegangstaCommand("create-service"),
					presentNonCodegangstaCommand("update-service"),
					presentNonCodegangstaCommand("delete-service"),
					presentNonCodegangstaCommand("rename-service"),
				}, {
					presentNonCodegangstaCommand("create-service-key"),
					presentNonCodegangstaCommand("service-keys"),
					presentNonCodegangstaCommand("service-key"),
					presentNonCodegangstaCommand("delete-service-key"),
				}, {
					presentNonCodegangstaCommand("bind-service"),
					presentNonCodegangstaCommand("unbind-service"),
				}, {
					presentNonCodegangstaCommand("create-user-provided-service"),
					presentNonCodegangstaCommand("update-user-provided-service"),
				},
			},
		}, {
			Name: T("ORGS"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("orgs"),
					presentNonCodegangstaCommand("org"),
				}, {
					presentNonCodegangstaCommand("create-org"),
					presentNonCodegangstaCommand("delete-org"),
					presentNonCodegangstaCommand("rename-org"),
				},
			},
		}, {
			Name: T("SPACES"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("spaces"),
					presentNonCodegangstaCommand("space"),
				}, {
					presentNonCodegangstaCommand("create-space"),
					presentNonCodegangstaCommand("delete-space"),
					presentNonCodegangstaCommand("rename-space"),
				}, {
					presentNonCodegangstaCommand("allow-space-ssh"),
					presentNonCodegangstaCommand("disallow-space-ssh"),
					presentNonCodegangstaCommand("space-ssh-allowed"),
				},
			},
		}, {
			Name: T("DOMAINS"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("domains"),
					presentNonCodegangstaCommand("create-domain"),
					presentNonCodegangstaCommand("delete-domain"),
					presentNonCodegangstaCommand("create-shared-domain"),
					presentNonCodegangstaCommand("delete-shared-domain"),
				},
			},
		}, {
			Name: T("ROUTES"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("routes"),
					presentNonCodegangstaCommand("create-route"),
					presentNonCodegangstaCommand("check-route"),
					presentNonCodegangstaCommand("map-route"),
					presentNonCodegangstaCommand("unmap-route"),
					presentNonCodegangstaCommand("delete-route"),
					presentNonCodegangstaCommand("delete-orphaned-routes"),
				},
			},
		}, {
			Name: T("BUILDPACKS"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("buildpacks"),
					presentNonCodegangstaCommand("create-buildpack"),
					presentNonCodegangstaCommand("update-buildpack"),
					presentNonCodegangstaCommand("rename-buildpack"),
					presentNonCodegangstaCommand("delete-buildpack"),
				},
			},
		}, {
			Name: T("USER ADMIN"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("create-user"),
					presentNonCodegangstaCommand("delete-user"),
				}, {
					presentNonCodegangstaCommand("org-users"),
					presentNonCodegangstaCommand("set-org-role"),
					presentNonCodegangstaCommand("unset-org-role"),
				}, {
					presentNonCodegangstaCommand("space-users"),
					presentNonCodegangstaCommand("set-space-role"),
					presentNonCodegangstaCommand("unset-space-role"),
				},
			},
		}, {
			Name: T("ORG ADMIN"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("quotas"),
					presentNonCodegangstaCommand("quota"),
					presentNonCodegangstaCommand("set-quota"),
				}, {
					presentNonCodegangstaCommand("create-quota"),
					presentNonCodegangstaCommand("delete-quota"),
					presentNonCodegangstaCommand("update-quota"),
				},
				{
					presentNonCodegangstaCommand("share-private-domain"),
					presentNonCodegangstaCommand("unshare-private-domain"),
				},
			},
		}, {
			Name: T("SPACE ADMIN"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("space-quotas"),
					presentNonCodegangstaCommand("space-quota"),
					presentNonCodegangstaCommand("create-space-quota"),
					presentNonCodegangstaCommand("update-space-quota"),
					presentNonCodegangstaCommand("delete-space-quota"),
					presentNonCodegangstaCommand("set-space-quota"),
					presentNonCodegangstaCommand("unset-space-quota"),
				},
			},
		}, {
			Name: T("SERVICE ADMIN"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("service-auth-tokens"),
					presentNonCodegangstaCommand("create-service-auth-token"),
					presentNonCodegangstaCommand("update-service-auth-token"),
					presentNonCodegangstaCommand("delete-service-auth-token"),
				}, {
					presentNonCodegangstaCommand("service-brokers"),
					presentNonCodegangstaCommand("create-service-broker"),
					presentNonCodegangstaCommand("update-service-broker"),
					presentNonCodegangstaCommand("delete-service-broker"),
					presentNonCodegangstaCommand("rename-service-broker"),
				}, {
					presentNonCodegangstaCommand("migrate-service-instances"),
					presentNonCodegangstaCommand("purge-service-offering"),
				}, {
					presentNonCodegangstaCommand("service-access"),
					presentNonCodegangstaCommand("enable-service-access"),
					presentNonCodegangstaCommand("disable-service-access"),
				},
			},
		}, {
			Name: T("SECURITY GROUP"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("security-group"),
					presentNonCodegangstaCommand("security-groups"),
					presentNonCodegangstaCommand("create-security-group"),
					presentNonCodegangstaCommand("update-security-group"),
					presentNonCodegangstaCommand("delete-security-group"),
					presentNonCodegangstaCommand("bind-security-group"),
					presentNonCodegangstaCommand("unbind-security-group"),
				}, {
					presentNonCodegangstaCommand("bind-staging-security-group"),
					presentNonCodegangstaCommand("staging-security-groups"),
					presentNonCodegangstaCommand("unbind-staging-security-group"),
				}, {
					presentNonCodegangstaCommand("bind-running-security-group"),
					presentNonCodegangstaCommand("running-security-groups"),
					presentNonCodegangstaCommand("unbind-running-security-group"),
				},
			},
		}, {
			Name: T("ENVIRONMENT VARIABLE GROUPS"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("running-environment-variable-group"),
					presentNonCodegangstaCommand("staging-environment-variable-group"),
					presentNonCodegangstaCommand("set-staging-environment-variable-group"),
					presentNonCodegangstaCommand("set-running-environment-variable-group"),
				},
			},
		},
		{
			Name: T("FEATURE FLAGS"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("feature-flags"),
					presentNonCodegangstaCommand("feature-flag"),
					presentNonCodegangstaCommand("enable-feature-flag"),
					presentNonCodegangstaCommand("disable-feature-flag"),
				},
			},
		}, {
			Name: T("ADVANCED"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("curl"),
					presentNonCodegangstaCommand("config"),
					presentNonCodegangstaCommand("oauth-token"),
				},
			},
		}, {
			Name: T("ADD/REMOVE PLUGIN REPOSITORY"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("add-plugin-repo"),
					presentNonCodegangstaCommand("remove-plugin-repo"),
					presentNonCodegangstaCommand("list-plugin-repos"),
					presentNonCodegangstaCommand("repo-plugins"),
				},
			},
		}, {
			Name: T("ADD/REMOVE PLUGIN"),
			CommandSubGroups: [][]cmdPresenter{
				{
					presentNonCodegangstaCommand("plugins"),
					presentNonCodegangstaCommand("install-plugin"),
					presentNonCodegangstaCommand("uninstall-plugin"),
				},
			},
		}, {
			Name: T("INSTALLED PLUGIN COMMANDS"),
			CommandSubGroups: [][]cmdPresenter{
				presentPluginCommands(),
			},
		},
	}

	return
}

func (p appPresenter) Title(name string) string {
	return terminal.HeaderColor(name)
}

func (c groupedCommands) SubTitle(name string) string {
	return terminal.HeaderColor(name + ":")
}
<|endoftext|>


<commit_before>package board

import (
	"math/rand"
	"time"
)

// Board size
const (
	X = 4
	Y = 4
)

// Direction
type Direction int32

// Direction
const (
	LEFT  = iota
	UP    = iota
	RIGHT = iota
	DOWN  = iota
)

var (
	DIRECTIONS = map[int]int{
		LEFT:  -1,
		UP:    -1,
		RIGHT: 1,
		DOWN:  1,
	}
)

type Board struct {
	Cells [][]int

	goal   int
	points int
}

func New() Board {
	board := Board{
		/*
		   Cells: [Y][X]int {
		       {0, 3, 0, 0},
		       {1, 0, 2, 0},
		       {2, 1, 1, 0},
		       {0, 6, 5, 0},
		   },
		*/
		Cells:  make2dArray(X, Y),
		goal:   2048,
		points: 0,
	}

	// Seed rng
	rand.Seed(time.Now().Unix())

	// Add two random tiles
	board.AddTile()
	board.AddTile()

	return board
}

func make2dArray(x, y int) [][]int {
	rows := make([][]int, y)

	for i, _ := range rows {
		rows[i] = make([]int, x)
	}

	return rows
}

func (b *Board) emptyRow(n int) []int {
	row := make([]int, n)
	return row[0:n]
}

func (b *Board) moveLine(row []int, direction int) []int {
	var empty []int
	var nonEmpty []int
	result := make([]int, len(row))

	for i := 0; i < len(row); i++ {
		if row[i] == 0 {
			continue
		}

		nonEmpty = append(nonEmpty, row[i])
	}

	empty = b.emptyRow(X - len(nonEmpty))

	// Copy merges to result array
	if direction == -1 {
		copy(result[:], append(nonEmpty, empty...)[0:len(row)])
	} else {
		copy(result[:], append(empty, nonEmpty...)[0:len(row)])
	}

	return result
}

// Is a given line mergeable
func canMergeLine(row []int) bool {
	for i := 0; i < len(row); i++ {
		// Previous
		if i > 0 && row[i] == row[i-1] {
			return true
		}

		// Next
		if i+1 < len(row) && row[i] == row[i+1] {
			return true
		}
	}

	return false
}

func (b *Board) mergeLine(row []int, direction int) []int {
	newRow := make([]int, len(row))
	var start, end, pos, nextpos int

	if direction == -1 {
		end = 0
		start = len(row) - 1
	} else {
		start = 0
		end = len(row) - 1
	}

	pos = start
	for i := 0; i < len(row); i++ {
		nextpos = pos + direction

		// Don't merge empty cells
		// or already merged cells
		if row[pos] == 0 || newRow[pos] != 0 {
			pos = nextpos
			continue
		}

		// Next cell is identical
		if pos != end && row[pos] == row[nextpos] {
			var value = row[pos] + 1
			newRow[pos] = 0
			newRow[nextpos] = value
		} else {
			newRow[pos] = row[pos]
		}

		// Update position
		pos = nextpos
	}

	return newRow
}

func (b *Board) setRow(y int, row []int) {
	for x := 0; x < X; x++ {
		b.Cells[y][x] = row[x]
	}
}

func (b *Board) getRow(y int) []int {
	return b.Cells[y]
}

func (b *Board) setCol(x int, row []int) {
	for y := 0; y < Y; y++ {
		b.Cells[y][x] = row[y]
	}
}

func (b *Board) getCol(x int) []int {
	a := make([]int, Y)

	for y := 0; y < Y; y++ {
		a[y] = b.Cells[y][x]
	}

	return a
}

func (b *Board) moveRows(d int) {
	for y := 0; y < Y; y++ {
		// Get new row by moving and merging previous row
		var newRow = b.moveLine(
			b.mergeLine(
				b.moveLine(
					b.getRow(y),
					d,
				),
				d,
			),
			d,
		)

		// Set new row
		b.setRow(y, newRow)
	}
}

func (b *Board) moveCols(d int) {
	for x := 0; x < X; x++ {
		// Get new col by moving and merging previous col
		var newCol = b.moveLine(
			b.mergeLine(
				b.moveLine(
					b.getCol(x),
					d,
				),
				d,
			),
			d,
		)

		// Set new col
		b.setCol(x, newCol)
	}
}

type cellLocation struct {
	x, y int
}

func (b *Board) emptyCells() []cellLocation {
	var arr []cellLocation

	for y := 0; y < Y; y++ {
		for x := 0; x < X; x++ {
			if b.Cells[y][x] == 0 {
				var cell = cellLocation{x, y}
				arr = append(arr, cell)
			}
		}
	}

	return arr
}

func (b *Board) IsFull() bool {
	return len(b.emptyCells()) == 0
}

func (b *Board) AddTile() {
	cells := b.emptyCells()
	cell := cells[rand.Int()%len(cells)]

	// Set cell randomly to 1 or 2
	// b.Cells[cell.y][cell.x] = (rand.Int() % 2) + 1
	b.Cells[cell.y][cell.x] = 1
}

func (b *Board) Playable() bool {
	if !b.IsFull() {
		return true
	}

	for y := 0; y < Y; y++ {
		if canMergeLine(b.getRow(y)) {
			return true
		}
	}

	for x := 0; x < X; x++ {
		if canMergeLine(b.getCol(x)) {
			return true
		}
	}

	return false
}

func (b *Board) Values() []int {
	var arr []int

	for y := 0; y < Y; y++ {
		for x := 0; x < X; x++ {
			if b.Cells[y][x] != 0 {
				arr = append(arr, b.Cells[y][x])
			}
		}
	}
	return arr
}

func copyCells(src [][]int) [][]int {
	dst := make2dArray(X, Y)
	for y := 0; y < Y; y++ {
		for x := 0; x < X; x++ {
			dst[y][x] = src[y][x]
		}
	}
	return dst
}

func cellsEqual(a, b [][]int) bool {
	for y := 0; y < Y; y++ {
		for x := 0; x < X; x++ {
			if a[y][x] != b[y][x] {
				return false
			}
		}
	}
	return true
}

// Move board in a given direction
func (b *Board) Move(d Direction) {
	// Make a copy of cells pre-moving (so we can see if anything changed)
	oldCells := copyCells(b.Cells)

	switch d {
	case UP:
		b.moveCols(DIRECTIONS[UP])
	case DOWN:
		b.moveCols(DIRECTIONS[DOWN])

	case LEFT:
		b.moveRows(DIRECTIONS[LEFT])
	case RIGHT:
		b.moveRows(DIRECTIONS[RIGHT])
	}

	// Don't add new tile if nothing in the board has changed
	cellsChanged := !cellsEqual(oldCells, b.Cells)

	// Add new tile if not empty
	if !b.IsFull() && cellsChanged {
		b.AddTile()
	}
}
<commit_msg>board: decouple line methods from Board struct to simplify testing<commit_after>package board

import (
	"math/rand"
	"time"
)

// Board size
const (
	X = 4
	Y = 4
)

// Direction
type Direction int32

// Direction
const (
	LEFT  = iota
	UP    = iota
	RIGHT = iota
	DOWN  = iota
)

var (
	DIRECTIONS = map[int]int{
		LEFT:  -1,
		UP:    -1,
		RIGHT: 1,
		DOWN:  1,
	}
)

type Board struct {
	Cells [][]int

	goal   int
	points int
}

func New() Board {
	board := Board{
		/*
		   Cells: [Y][X]int {
		       {0, 3, 0, 0},
		       {1, 0, 2, 0},
		       {2, 1, 1, 0},
		       {0, 6, 5, 0},
		   },
		*/
		Cells:  make2dArray(X, Y),
		goal:   2048,
		points: 0,
	}

	// Seed rng
	rand.Seed(time.Now().Unix())

	// Add two random tiles
	board.AddTile()
	board.AddTile()

	return board
}

func make2dArray(x, y int) [][]int {
	rows := make([][]int, y)

	for i, _ := range rows {
		rows[i] = make([]int, x)
	}

	return rows
}

func emptyRow(n int) []int {
	row := make([]int, n)
	return row[0:n]
}

func moveLine(row []int, direction int) []int {
	var empty []int
	var nonEmpty []int
	result := make([]int, len(row))

	for i := 0; i < len(row); i++ {
		if row[i] == 0 {
			continue
		}

		nonEmpty = append(nonEmpty, row[i])
	}

	empty = emptyRow(X - len(nonEmpty))

	// Copy merges to result array
	if direction == -1 {
		copy(result[:], append(nonEmpty, empty...)[0:len(row)])
	} else {
		copy(result[:], append(empty, nonEmpty...)[0:len(row)])
	}

	return result
}

// Is a given line mergeable
func canMergeLine(row []int) bool {
	for i := 0; i < len(row); i++ {
		// Previous
		if i > 0 && row[i] == row[i-1] {
			return true
		}

		// Next
		if i+1 < len(row) && row[i] == row[i+1] {
			return true
		}
	}

	return false
}

func mergeLine(row []int, direction int) []int {
	newRow := make([]int, len(row))
	var start, end, pos, nextpos int

	if direction == -1 {
		end = 0
		start = len(row) - 1
	} else {
		start = 0
		end = len(row) - 1
	}

	pos = start
	for i := 0; i < len(row); i++ {
		nextpos = pos + direction

		// Don't merge empty cells
		// or already merged cells
		if row[pos] == 0 || newRow[pos] != 0 {
			pos = nextpos
			continue
		}

		// Next cell is identical
		if pos != end && row[pos] == row[nextpos] {
			var value = row[pos] + 1
			newRow[pos] = 0
			newRow[nextpos] = value
		} else {
			newRow[pos] = row[pos]
		}

		// Update position
		pos = nextpos
	}

	return newRow
}

func (b *Board) setRow(y int, row []int) {
	for x := 0; x < X; x++ {
		b.Cells[y][x] = row[x]
	}
}

func (b *Board) getRow(y int) []int {
	return b.Cells[y]
}

func (b *Board) setCol(x int, row []int) {
	for y := 0; y < Y; y++ {
		b.Cells[y][x] = row[y]
	}
}

func (b *Board) getCol(x int) []int {
	a := make([]int, Y)

	for y := 0; y < Y; y++ {
		a[y] = b.Cells[y][x]
	}

	return a
}

func (b *Board) moveRows(d int) {
	for y := 0; y < Y; y++ {
		// Get new row by moving and merging previous row
		var newRow = moveLine(
			mergeLine(
				moveLine(
					b.getRow(y),
					d,
				),
				d,
			),
			d,
		)

		// Set new row
		b.setRow(y, newRow)
	}
}

func (b *Board) moveCols(d int) {
	for x := 0; x < X; x++ {
		// Get new col by moving and merging previous col
		var newCol = moveLine(
			mergeLine(
				moveLine(
					b.getCol(x),
					d,
				),
				d,
			),
			d,
		)

		// Set new col
		b.setCol(x, newCol)
	}
}

type cellLocation struct {
	x, y int
}

func (b *Board) emptyCells() []cellLocation {
	var arr []cellLocation

	for y := 0; y < Y; y++ {
		for x := 0; x < X; x++ {
			if b.Cells[y][x] == 0 {
				var cell = cellLocation{x, y}
				arr = append(arr, cell)
			}
		}
	}

	return arr
}

func (b *Board) IsFull() bool {
	return len(b.emptyCells()) == 0
}

func (b *Board) AddTile() {
	cells := b.emptyCells()
	cell := cells[rand.Int()%len(cells)]

	// Set cell randomly to 1 or 2
	// b.Cells[cell.y][cell.x] = (rand.Int() % 2) + 1
	b.Cells[cell.y][cell.x] = 1
}

func (b *Board) Playable() bool {
	if !b.IsFull() {
		return true
	}

	for y := 0; y < Y; y++ {
		if canMergeLine(b.getRow(y)) {
			return true
		}
	}

	for x := 0; x < X; x++ {
		if canMergeLine(b.getCol(x)) {
			return true
		}
	}

	return false
}

func (b *Board) Values() []int {
	var arr []int

	for y := 0; y < Y; y++ {
		for x := 0; x < X; x++ {
			if b.Cells[y][x] != 0 {
				arr = append(arr, b.Cells[y][x])
			}
		}
	}
	return arr
}

func copyCells(src [][]int) [][]int {
	dst := make2dArray(X, Y)
	for y := 0; y < Y; y++ {
		for x := 0; x < X; x++ {
			dst[y][x] = src[y][x]
		}
	}
	return dst
}

func cellsEqual(a, b [][]int) bool {
	for y := 0; y < Y; y++ {
		for x := 0; x < X; x++ {
			if a[y][x] != b[y][x] {
				return false
			}
		}
	}
	return true
}

// Move board in a given direction
func (b *Board) Move(d Direction) {
	// Make a copy of cells pre-moving (so we can see if anything changed)
	oldCells := copyCells(b.Cells)

	switch d {
	case UP:
		b.moveCols(DIRECTIONS[UP])
	case DOWN:
		b.moveCols(DIRECTIONS[DOWN])

	case LEFT:
		b.moveRows(DIRECTIONS[LEFT])
	case RIGHT:
		b.moveRows(DIRECTIONS[RIGHT])
	}

	// Don't add new tile if nothing in the board has changed
	cellsChanged := !cellsEqual(oldCells, b.Cells)

	// Add new tile if not empty
	if !b.IsFull() && cellsChanged {
		b.AddTile()
	}
}
<|endoftext|>


<commit_before>/*

	memory is a storage manager that just keeps the games and storage in
	memory, which means that when the program exits the storage evaporates.
	Useful in cases where you don't want a persistent store (e.g. testing or
	fast iteration). Implements both boardgame.StorageManager and
	boardgame/server.StorageManager.

*/
package memory

import (
	"errors"
	"github.com/jkomoros/boardgame"
	"github.com/jkomoros/boardgame/server/api/extendedgame"
	"github.com/jkomoros/boardgame/server/api/listing"
	"github.com/jkomoros/boardgame/server/api/users"
	"sort"
	"sync"
	"time"
)

type StorageManager struct {
	states            map[string]map[int]boardgame.StateStorageRecord
	moves             map[string]map[int]*boardgame.MoveStorageRecord
	games             map[string]*boardgame.GameStorageRecord
	extendedGames     map[string]*extendedgame.StorageRecord
	usersById         map[string]*users.StorageRecord
	usersByCookie     map[string]*users.StorageRecord
	usersForGames     map[string][]string
	agentStates       map[string][]byte
	statesLock        sync.RWMutex
	movesLock         sync.RWMutex
	gamesLock         sync.RWMutex
	extendedGamesLock sync.RWMutex
	usersLock         sync.RWMutex
	usersForGamesLock sync.RWMutex
	agentStatesLock   sync.RWMutex
}

func NewStorageManager() *StorageManager {
	//InMemoryStorageManager is an extremely simple StorageManager that just keeps
	//track of the objects in memory.
	return &StorageManager{
		states:        make(map[string]map[int]boardgame.StateStorageRecord),
		moves:         make(map[string]map[int]*boardgame.MoveStorageRecord),
		games:         make(map[string]*boardgame.GameStorageRecord),
		extendedGames: make(map[string]*extendedgame.StorageRecord),
		usersById:     make(map[string]*users.StorageRecord),
		usersByCookie: make(map[string]*users.StorageRecord),
		usersForGames: make(map[string][]string),
		agentStates:   make(map[string][]byte),
	}
}

func (s *StorageManager) Name() string {
	return "memory"
}

func (s *StorageManager) State(gameId string, version int) (boardgame.StateStorageRecord, error) {
	if gameId == "" {
		return nil, errors.New("No game provided")
	}

	if version < 0 {
		return nil, errors.New("Invalid version")
	}

	s.statesLock.RLock()

	versionMap, ok := s.states[gameId]

	s.statesLock.RUnlock()

	if !ok {
		return nil, errors.New("No such game")
	}
	s.statesLock.RLock()
	record, ok := versionMap[version]
	s.statesLock.RUnlock()

	if !ok {
		return nil, errors.New("No such version for that game")
	}

	return record, nil

}

func (s *StorageManager) Moves(gameId string, fromVersion, toVersion int) ([]*boardgame.MoveStorageRecord, error) {

	//There's no efficiency boost for fetching multiple moves at once so just wrap around Move()

	if fromVersion == toVersion {
		fromVersion = fromVersion - 1
	}

	result := make([]*boardgame.MoveStorageRecord, toVersion-fromVersion)

	index := 0
	for i := fromVersion + 1; i <= toVersion; i++ {
		move, err := s.Move(gameId, i)
		if err != nil {
			return nil, err
		}
		result[index] = move
		index++
	}
	return result, nil
}

func (s *StorageManager) Move(gameId string, version int) (*boardgame.MoveStorageRecord, error) {
	if gameId == "" {
		return nil, errors.New("No game provided")
	}

	if version < 0 {
		return nil, errors.New("Invalid version")
	}

	s.movesLock.RLock()

	versionMap, ok := s.moves[gameId]

	s.movesLock.RUnlock()

	if !ok {
		return nil, errors.New("No such game")
	}
	s.movesLock.RLock()
	record, ok := versionMap[version]
	s.movesLock.RUnlock()

	if !ok {
		return nil, errors.New("No such version for that game")
	}

	return record, nil

}

func (s *StorageManager) Game(id string) (*boardgame.GameStorageRecord, error) {

	s.gamesLock.RLock()
	record := s.games[id]
	s.gamesLock.RUnlock()

	if record == nil {
		return nil, errors.New("No such game")
	}

	return record, nil
}

func (s *StorageManager) SaveGameAndCurrentState(game *boardgame.GameStorageRecord, state boardgame.StateStorageRecord, move *boardgame.MoveStorageRecord) error {
	if game == nil {
		return errors.New("No game provided")
	}

	s.statesLock.RLock()
	_, ok := s.states[game.Id]
	s.statesLock.RUnlock()
	if !ok {
		s.statesLock.Lock()
		s.states[game.Id] = make(map[int]boardgame.StateStorageRecord)
		s.statesLock.Unlock()
	}

	s.movesLock.RLock()
	_, ok = s.moves[game.Id]
	s.movesLock.RUnlock()
	if !ok {
		s.movesLock.Lock()
		s.moves[game.Id] = make(map[int]*boardgame.MoveStorageRecord)
		s.movesLock.Unlock()
	}

	version := game.Version

	s.statesLock.RLock()
	versionMap := s.states[game.Id]
	_, ok = versionMap[version]
	s.statesLock.RUnlock()

	if ok {
		//Wait, there was already a version stored there?
		return errors.New("There was already a version for that game stored")
	}

	s.movesLock.RLock()
	moveMap := s.moves[game.Id]
	_, ok = moveMap[version]
	s.movesLock.RUnlock()

	if ok {
		//Wait, there was already a version stored there?
		return errors.New("There was already a version for that game stored")
	}

	s.extendedGamesLock.RLock()
	eGame, ok := s.extendedGames[game.Id]
	s.extendedGamesLock.RUnlock()
	if !ok {
		s.extendedGamesLock.Lock()
		s.extendedGames[game.Id] = extendedgame.DefaultStorageRecord()
		s.extendedGamesLock.Unlock()
	} else {
		eGame.LastActivity = time.Now().UnixNano()
	}

	s.statesLock.Lock()
	versionMap[version] = state
	s.statesLock.Unlock()

	s.movesLock.Lock()
	moveMap[version] = move
	s.movesLock.Unlock()

	s.gamesLock.Lock()
	s.games[game.Id] = game
	s.gamesLock.Unlock()

	return nil
}

func keyForAgent(gameId string, player boardgame.PlayerIndex) string {
	return gameId + "-" + player.String()
}

func (s *StorageManager) AgentState(gameId string, player boardgame.PlayerIndex) ([]byte, error) {

	key := keyForAgent(gameId, player)

	s.agentStatesLock.RLock()
	result := s.agentStates[key]
	s.agentStatesLock.RUnlock()

	return result, nil
}

func (s *StorageManager) SaveAgentState(gameId string, player boardgame.PlayerIndex, state []byte) error {
	key := keyForAgent(gameId, player)

	s.agentStatesLock.Lock()
	s.agentStates[key] = state
	s.agentStatesLock.Unlock()

	return nil
}

//ListGames will return game objects for up to max number of games
func (s *StorageManager) ListGames(max int, list listing.Type, userId string, gameType string) []*extendedgame.CombinedStorageRecord {

	if (list == listing.ParticipatingActive || list == listing.ParticipatingActive) && userId == "" {
		//If we're filtering to only participating games and there's no userId, then there can't be any games,
		//because the non-user can't be participating in any games.
		return nil
	}

	var result []*extendedgame.CombinedStorageRecord

	for _, game := range s.games {

		if gameType != "" {
			if game.Name != gameType {
				continue
			}
		}

		eGame := s.extendedGames[game.Id]

		usersForGame := s.UserIdsForGame(game.Id)

		hasUser := false
		numUsers := 0

		for _, user := range usersForGame {
			if user != "" {
				numUsers++
			}
			if userId != "" && user == userId {
				hasUser = true
				break
			}
		}

		numAgents := 0

		for _, agent := range game.Agents {
			if agent != "" {
				numAgents++
			}
		}

		hasSlots := game.NumPlayers > (numUsers + numAgents)

		switch list {
		case listing.ParticipatingActive:
			if game.Finished || !hasUser {
				continue
			}
		case listing.ParticipatingFinished:
			if !game.Finished || !hasUser {
				continue
			}
		case listing.VisibleJoinableActive:
			if game.Finished || hasUser || !eGame.Visible || !eGame.Open || !hasSlots {
				continue
			}
		case listing.VisibleActive:
			if game.Finished || hasUser || !eGame.Visible || (eGame.Open && hasSlots) {
				continue
			}
		}

		result = append(result, &extendedgame.CombinedStorageRecord{
			*game,
			*eGame,
		})

		if len(result) >= max {
			break
		}
	}

	sort.Slice(result, func(i, j int) bool {
		return result[i].LastActivity > result[j].LastActivity
	})

	return result

}

func (s *StorageManager) ExtendedGame(id string) (*extendedgame.StorageRecord, error) {
	s.extendedGamesLock.RLock()
	eGame := s.extendedGames[id]
	s.extendedGamesLock.RUnlock()
	if eGame == nil {
		return nil, errors.New("No such extended game")
	}

	return eGame, nil
}

func (s *StorageManager) CombinedGame(id string) (*extendedgame.CombinedStorageRecord, error) {
	s.extendedGamesLock.RLock()
	eGame := s.extendedGames[id]
	s.extendedGamesLock.RUnlock()
	if eGame == nil {
		return nil, errors.New("No such extended game")
	}

	game, err := s.Game(id)

	if err != nil {
		return nil, err
	}

	result := &extendedgame.CombinedStorageRecord{
		*game,
		*eGame,
	}

	return result, nil
}

func (s *StorageManager) UpdateExtendedGame(id string, eGame *extendedgame.StorageRecord) error {
	s.extendedGamesLock.Lock()
	s.extendedGames[id] = eGame
	s.extendedGamesLock.Unlock()
	return nil
}

func (s *StorageManager) UserIdsForGame(gameId string) []string {
	s.usersForGamesLock.RLock()
	ids := s.usersForGames[gameId]
	s.usersForGamesLock.RUnlock()

	if ids == nil {
		game, _ := s.Game(gameId)
		if game == nil {
			return nil
		}
		return make([]string, game.NumPlayers)
	}

	return ids
}

func (s *StorageManager) SetPlayerForGame(gameId string, playerIndex boardgame.PlayerIndex, userId string) error {
	ids := s.UserIdsForGame(gameId)

	if int(playerIndex) < 0 || int(playerIndex) >= len(ids) {
		return errors.New("PlayerIndex " + playerIndex.String() + " is not valid for this game.")
	}

	if ids[playerIndex] != "" {
		return errors.New("PlayerIndex " + playerIndex.String() + " is already taken.")
	}

	user := s.GetUserById(userId)

	if user == nil {
		return errors.New("That uid does not describe an existing user")
	}

	ids[playerIndex] = userId

	s.usersForGamesLock.Lock()
	s.usersForGames[gameId] = ids
	s.usersForGamesLock.Unlock()

	return nil
}

//Store or update all fields
func (s *StorageManager) UpdateUser(user *users.StorageRecord) error {

	s.usersLock.Lock()
	s.usersById[user.Id] = user
	s.usersLock.Unlock()

	return nil

}

func (s *StorageManager) GetUserById(uid string) *users.StorageRecord {
	s.usersLock.RLock()
	user := s.usersById[uid]
	s.usersLock.RUnlock()

	return user
}

func (s *StorageManager) GetUserByCookie(cookie string) *users.StorageRecord {
	s.usersLock.RLock()
	user := s.usersByCookie[cookie]
	s.usersLock.RUnlock()

	return user
}

//If user is nil, the cookie should be deleted if it exists. If the user
//does not yet exist, it should be added to the database.
func (s *StorageManager) ConnectCookieToUser(cookie string, user *users.StorageRecord) error {
	if user == nil {
		s.usersLock.Lock()
		delete(s.usersByCookie, cookie)
		s.usersLock.Unlock()
		return nil
	}

	otherUser := s.GetUserById(user.Id)

	if otherUser == nil {
		s.UpdateUser(user)
	}

	s.usersLock.Lock()
	s.usersByCookie[cookie] = user
	s.usersLock.Unlock()

	return nil
}

func (s *StorageManager) Connect(config string) error {
	return nil
}

func (s *StorageManager) Close() {
	//Don't need to do anything
}

func (s *StorageManager) CleanUp() {
	//Don't need to do
}

func (s *StorageManager) PlayerMoveApplied(game *boardgame.GameStorageRecord) error {
	//Don't need to do anything
	return nil
}
<commit_msg>Fix a bug in the memory storage layer where on the very first save of a game we saved a nil move record. Only found now because the machinery for #516 exercised it.<commit_after>/*

	memory is a storage manager that just keeps the games and storage in
	memory, which means that when the program exits the storage evaporates.
	Useful in cases where you don't want a persistent store (e.g. testing or
	fast iteration). Implements both boardgame.StorageManager and
	boardgame/server.StorageManager.

*/
package memory

import (
	"errors"
	"github.com/jkomoros/boardgame"
	"github.com/jkomoros/boardgame/server/api/extendedgame"
	"github.com/jkomoros/boardgame/server/api/listing"
	"github.com/jkomoros/boardgame/server/api/users"
	"sort"
	"sync"
	"time"
)

type StorageManager struct {
	states            map[string]map[int]boardgame.StateStorageRecord
	moves             map[string]map[int]*boardgame.MoveStorageRecord
	games             map[string]*boardgame.GameStorageRecord
	extendedGames     map[string]*extendedgame.StorageRecord
	usersById         map[string]*users.StorageRecord
	usersByCookie     map[string]*users.StorageRecord
	usersForGames     map[string][]string
	agentStates       map[string][]byte
	statesLock        sync.RWMutex
	movesLock         sync.RWMutex
	gamesLock         sync.RWMutex
	extendedGamesLock sync.RWMutex
	usersLock         sync.RWMutex
	usersForGamesLock sync.RWMutex
	agentStatesLock   sync.RWMutex
}

func NewStorageManager() *StorageManager {
	//InMemoryStorageManager is an extremely simple StorageManager that just keeps
	//track of the objects in memory.
	return &StorageManager{
		states:        make(map[string]map[int]boardgame.StateStorageRecord),
		moves:         make(map[string]map[int]*boardgame.MoveStorageRecord),
		games:         make(map[string]*boardgame.GameStorageRecord),
		extendedGames: make(map[string]*extendedgame.StorageRecord),
		usersById:     make(map[string]*users.StorageRecord),
		usersByCookie: make(map[string]*users.StorageRecord),
		usersForGames: make(map[string][]string),
		agentStates:   make(map[string][]byte),
	}
}

func (s *StorageManager) Name() string {
	return "memory"
}

func (s *StorageManager) State(gameId string, version int) (boardgame.StateStorageRecord, error) {
	if gameId == "" {
		return nil, errors.New("No game provided")
	}

	if version < 0 {
		return nil, errors.New("Invalid version")
	}

	s.statesLock.RLock()

	versionMap, ok := s.states[gameId]

	s.statesLock.RUnlock()

	if !ok {
		return nil, errors.New("No such game")
	}
	s.statesLock.RLock()
	record, ok := versionMap[version]
	s.statesLock.RUnlock()

	if !ok {
		return nil, errors.New("No such version for that game")
	}

	return record, nil

}

func (s *StorageManager) Moves(gameId string, fromVersion, toVersion int) ([]*boardgame.MoveStorageRecord, error) {

	//There's no efficiency boost for fetching multiple moves at once so just wrap around Move()

	if fromVersion == toVersion {
		fromVersion = fromVersion - 1
	}

	result := make([]*boardgame.MoveStorageRecord, toVersion-fromVersion)

	index := 0
	for i := fromVersion + 1; i <= toVersion; i++ {
		move, err := s.Move(gameId, i)
		if err != nil {
			return nil, err
		}
		result[index] = move
		index++
	}
	return result, nil
}

func (s *StorageManager) Move(gameId string, version int) (*boardgame.MoveStorageRecord, error) {
	if gameId == "" {
		return nil, errors.New("No game provided")
	}

	if version < 0 {
		return nil, errors.New("Invalid version")
	}

	s.movesLock.RLock()

	versionMap, ok := s.moves[gameId]

	s.movesLock.RUnlock()

	if !ok {
		return nil, errors.New("No such game")
	}
	s.movesLock.RLock()
	record, ok := versionMap[version]
	s.movesLock.RUnlock()

	if !ok {
		return nil, errors.New("No such version for that game")
	}

	return record, nil

}

func (s *StorageManager) Game(id string) (*boardgame.GameStorageRecord, error) {

	s.gamesLock.RLock()
	record := s.games[id]
	s.gamesLock.RUnlock()

	if record == nil {
		return nil, errors.New("No such game")
	}

	return record, nil
}

func (s *StorageManager) SaveGameAndCurrentState(game *boardgame.GameStorageRecord, state boardgame.StateStorageRecord, move *boardgame.MoveStorageRecord) error {
	if game == nil {
		return errors.New("No game provided")
	}

	s.statesLock.RLock()
	_, ok := s.states[game.Id]
	s.statesLock.RUnlock()
	if !ok {
		s.statesLock.Lock()
		s.states[game.Id] = make(map[int]boardgame.StateStorageRecord)
		s.statesLock.Unlock()
	}

	s.movesLock.RLock()
	_, ok = s.moves[game.Id]
	s.movesLock.RUnlock()
	if !ok {
		s.movesLock.Lock()
		s.moves[game.Id] = make(map[int]*boardgame.MoveStorageRecord)
		s.movesLock.Unlock()
	}

	version := game.Version

	s.statesLock.RLock()
	versionMap := s.states[game.Id]
	_, ok = versionMap[version]
	s.statesLock.RUnlock()

	if ok {
		//Wait, there was already a version stored there?
		return errors.New("There was already a version for that game stored")
	}

	s.movesLock.RLock()
	moveMap := s.moves[game.Id]
	_, ok = moveMap[version]
	s.movesLock.RUnlock()

	if ok {
		//Wait, there was already a version stored there?
		return errors.New("There was already a version for that game stored")
	}

	s.extendedGamesLock.RLock()
	eGame, ok := s.extendedGames[game.Id]
	s.extendedGamesLock.RUnlock()
	if !ok {
		s.extendedGamesLock.Lock()
		s.extendedGames[game.Id] = extendedgame.DefaultStorageRecord()
		s.extendedGamesLock.Unlock()
	} else {
		eGame.LastActivity = time.Now().UnixNano()
	}

	s.statesLock.Lock()
	versionMap[version] = state
	s.statesLock.Unlock()

	s.movesLock.Lock()
	if move != nil {
		moveMap[version] = move
	}
	s.movesLock.Unlock()

	s.gamesLock.Lock()
	s.games[game.Id] = game
	s.gamesLock.Unlock()

	return nil
}

func keyForAgent(gameId string, player boardgame.PlayerIndex) string {
	return gameId + "-" + player.String()
}

func (s *StorageManager) AgentState(gameId string, player boardgame.PlayerIndex) ([]byte, error) {

	key := keyForAgent(gameId, player)

	s.agentStatesLock.RLock()
	result := s.agentStates[key]
	s.agentStatesLock.RUnlock()

	return result, nil
}

func (s *StorageManager) SaveAgentState(gameId string, player boardgame.PlayerIndex, state []byte) error {
	key := keyForAgent(gameId, player)

	s.agentStatesLock.Lock()
	s.agentStates[key] = state
	s.agentStatesLock.Unlock()

	return nil
}

//ListGames will return game objects for up to max number of games
func (s *StorageManager) ListGames(max int, list listing.Type, userId string, gameType string) []*extendedgame.CombinedStorageRecord {

	if (list == listing.ParticipatingActive || list == listing.ParticipatingActive) && userId == "" {
		//If we're filtering to only participating games and there's no userId, then there can't be any games,
		//because the non-user can't be participating in any games.
		return nil
	}

	var result []*extendedgame.CombinedStorageRecord

	for _, game := range s.games {

		if gameType != "" {
			if game.Name != gameType {
				continue
			}
		}

		eGame := s.extendedGames[game.Id]

		usersForGame := s.UserIdsForGame(game.Id)

		hasUser := false
		numUsers := 0

		for _, user := range usersForGame {
			if user != "" {
				numUsers++
			}
			if userId != "" && user == userId {
				hasUser = true
				break
			}
		}

		numAgents := 0

		for _, agent := range game.Agents {
			if agent != "" {
				numAgents++
			}
		}

		hasSlots := game.NumPlayers > (numUsers + numAgents)

		switch list {
		case listing.ParticipatingActive:
			if game.Finished || !hasUser {
				continue
			}
		case listing.ParticipatingFinished:
			if !game.Finished || !hasUser {
				continue
			}
		case listing.VisibleJoinableActive:
			if game.Finished || hasUser || !eGame.Visible || !eGame.Open || !hasSlots {
				continue
			}
		case listing.VisibleActive:
			if game.Finished || hasUser || !eGame.Visible || (eGame.Open && hasSlots) {
				continue
			}
		}

		result = append(result, &extendedgame.CombinedStorageRecord{
			*game,
			*eGame,
		})

		if len(result) >= max {
			break
		}
	}

	sort.Slice(result, func(i, j int) bool {
		return result[i].LastActivity > result[j].LastActivity
	})

	return result

}

func (s *StorageManager) ExtendedGame(id string) (*extendedgame.StorageRecord, error) {
	s.extendedGamesLock.RLock()
	eGame := s.extendedGames[id]
	s.extendedGamesLock.RUnlock()
	if eGame == nil {
		return nil, errors.New("No such extended game")
	}

	return eGame, nil
}

func (s *StorageManager) CombinedGame(id string) (*extendedgame.CombinedStorageRecord, error) {
	s.extendedGamesLock.RLock()
	eGame := s.extendedGames[id]
	s.extendedGamesLock.RUnlock()
	if eGame == nil {
		return nil, errors.New("No such extended game")
	}

	game, err := s.Game(id)

	if err != nil {
		return nil, err
	}

	result := &extendedgame.CombinedStorageRecord{
		*game,
		*eGame,
	}

	return result, nil
}

func (s *StorageManager) UpdateExtendedGame(id string, eGame *extendedgame.StorageRecord) error {
	s.extendedGamesLock.Lock()
	s.extendedGames[id] = eGame
	s.extendedGamesLock.Unlock()
	return nil
}

func (s *StorageManager) UserIdsForGame(gameId string) []string {
	s.usersForGamesLock.RLock()
	ids := s.usersForGames[gameId]
	s.usersForGamesLock.RUnlock()

	if ids == nil {
		game, _ := s.Game(gameId)
		if game == nil {
			return nil
		}
		return make([]string, game.NumPlayers)
	}

	return ids
}

func (s *StorageManager) SetPlayerForGame(gameId string, playerIndex boardgame.PlayerIndex, userId string) error {
	ids := s.UserIdsForGame(gameId)

	if int(playerIndex) < 0 || int(playerIndex) >= len(ids) {
		return errors.New("PlayerIndex " + playerIndex.String() + " is not valid for this game.")
	}

	if ids[playerIndex] != "" {
		return errors.New("PlayerIndex " + playerIndex.String() + " is already taken.")
	}

	user := s.GetUserById(userId)

	if user == nil {
		return errors.New("That uid does not describe an existing user")
	}

	ids[playerIndex] = userId

	s.usersForGamesLock.Lock()
	s.usersForGames[gameId] = ids
	s.usersForGamesLock.Unlock()

	return nil
}

//Store or update all fields
func (s *StorageManager) UpdateUser(user *users.StorageRecord) error {

	s.usersLock.Lock()
	s.usersById[user.Id] = user
	s.usersLock.Unlock()

	return nil

}

func (s *StorageManager) GetUserById(uid string) *users.StorageRecord {
	s.usersLock.RLock()
	user := s.usersById[uid]
	s.usersLock.RUnlock()

	return user
}

func (s *StorageManager) GetUserByCookie(cookie string) *users.StorageRecord {
	s.usersLock.RLock()
	user := s.usersByCookie[cookie]
	s.usersLock.RUnlock()

	return user
}

//If user is nil, the cookie should be deleted if it exists. If the user
//does not yet exist, it should be added to the database.
func (s *StorageManager) ConnectCookieToUser(cookie string, user *users.StorageRecord) error {
	if user == nil {
		s.usersLock.Lock()
		delete(s.usersByCookie, cookie)
		s.usersLock.Unlock()
		return nil
	}

	otherUser := s.GetUserById(user.Id)

	if otherUser == nil {
		s.UpdateUser(user)
	}

	s.usersLock.Lock()
	s.usersByCookie[cookie] = user
	s.usersLock.Unlock()

	return nil
}

func (s *StorageManager) Connect(config string) error {
	return nil
}

func (s *StorageManager) Close() {
	//Don't need to do anything
}

func (s *StorageManager) CleanUp() {
	//Don't need to do
}

func (s *StorageManager) PlayerMoveApplied(game *boardgame.GameStorageRecord) error {
	//Don't need to do anything
	return nil
}
<|endoftext|>


<commit_before>package ring_test

import (
	"bytes"
	"encoding/json"
	"fmt"
	"hash/fnv"

	"github.com/gholt/ring"
)

// This will be an in-depth implementation of using the ring package. We will
// be building a distributed object storage system where object names will be
// mapped to disks. There will be multiple disks per server, and multiple
// servers per zone.

// First, let's define our BuilderDisk, representing a single disk in the
// cluster and is what the ring package will be mapping assignments to.

// We want all the fields to be private because we don't want users of our new
// package to accidentally alter anything. This complicates our code, but
// simplifies things for users of our new storage package, and would be a
// common use case.

type BuilderDisk struct {
	// We'll define our specific StorageBuilder later. We need a reference to
	// it in each node so we notify it of changes and to resolve things like
	// tier names.
	storageBuilder *StorageBuilder
	// This is the actual ring.Node the ring.Builder will need to work with,
	// and contains whether the disk is Disabled or not, the Capacity, and the
	// list of indexes that define what tiers (server, zone) the disk is in.
	node ring.Node
	// The ip:port where the disk can be reached.
	addr string
	// The name of the disk on the server.
	name string
}

// Now we map public methods to the fields.

func (bd *BuilderDisk) Disabled() bool {
	return bd.node.Disabled
}

func (bd *BuilderDisk) SetDisabled(v bool) {
	bd.node.Disabled = v
}

func (bd *BuilderDisk) Capacity() uint32 {
	return bd.node.Capacity
}

func (bd *BuilderDisk) SetCapacity(v uint32) {
	bd.node.Capacity = v
}

func (bd *BuilderDisk) Tiers() []string {
	tiers := make([]string, len(bd.node.TierIndexes))
	for i, ti := range bd.node.TierIndexes {
		tiers[i] = bd.storageBuilder.tierIndexToName[ti]
	}
	return tiers
}

func (bd *BuilderDisk) SetTier(tier int, name string) {
	for tier >= len(bd.node.TierIndexes) {
		bd.node.TierIndexes = append(bd.node.TierIndexes, 0)
	}
	if bd.storageBuilder.tierNameToIndex == nil {
		bd.storageBuilder.tierNameToIndex = map[string]uint32{}
	}
	if ti, ok := bd.storageBuilder.tierNameToIndex[name]; ok {
		bd.node.TierIndexes[tier] = ti
	} else {
		ti = uint32(len(bd.storageBuilder.tierIndexToName))
		bd.storageBuilder.tierIndexToName = append(bd.storageBuilder.tierIndexToName, name)
		bd.storageBuilder.tierNameToIndex[name] = ti
		bd.node.TierIndexes[tier] = ti
	}
}

func (bd *BuilderDisk) Addr() string {
	return bd.addr
}

func (bd *BuilderDisk) SetAddr(v string) {
	bd.addr = v
}

func (bd *BuilderDisk) Name() string {
	return bd.name
}

func (bd *BuilderDisk) SetName(v string) {
	bd.name = v
}

// We want to be able to persist this information, so we make JSON translators.
// Note that this does not set the storageBuilder field; that has to be done by
// the StorageBuilder JSON translators.

type builderDiskJSON struct {
	// ring.Node is serializable on its own.
	Node *ring.Node
	Addr string
	Name string
}

func (bd *BuilderDisk) MarshalJSON() ([]byte, error) {
	return json.Marshal(&builderDiskJSON{
		Node: &bd.node,
		Addr: bd.addr,
		Name: bd.name,
	})
}

func (bd *BuilderDisk) UnmarshalJSON(b []byte) error {
	var j builderDiskJSON
	if err := json.Unmarshal(b, &j); err != nil {
		return err
	}
	bd.node = *j.Node
	bd.addr = j.Addr
	bd.name = j.Name
	return nil
}

// Now, let's define the StorageBuilder.

type StorageBuilder struct {
	// This is the actual builder that will do all the reblancing work.
	builder ring.Builder
	// These are all the disks in the cluster, we have to map them to the
	// ring.Builder's nodes.
	disks []*BuilderDisk
	// These are to give the tiers human readable names, like Server34 and
	// Zone5 or whatever is desired.
	tierIndexToName []string
	tierNameToIndex map[string]uint32
}

func (sb *StorageBuilder) AddDisk() *BuilderDisk {
	bd := &BuilderDisk{storageBuilder: sb}
	sb.disks = append(sb.disks, bd)
	sb.builder.Nodes = append(sb.builder.Nodes, &bd.node)
	return bd
}

func (sb *StorageBuilder) ChangeReplicaCount(count int) {
	sb.builder.ChangeReplicaCount(count)
}

// We'll skip removing disks, listing all disks, reading the replica count,
// partition count, changing last moved, etc. but those kind of methods would
// normally exist. The important point is that any modifications need to keep
// the ring.Builder's nodes in sync.

// We do want to give a useful ring to use, so let's provide that method. We'll
// define the StorageRing later, but it will be an immutable copy of the state
// of things at the time of StorageRing() call.

func (sb *StorageBuilder) StorageRing() *StorageRing {
	sb.builder.Rebalance()
	storageRing := &StorageRing{ring: sb.builder.RingDuplicate()}
	storageRing.disks = make([]*StorageDisk, len(sb.disks))
	for i, d := range sb.disks {
		storageRing.disks[i] = &StorageDisk{
			storageRing: storageRing,
			disabled:    d.node.Disabled,
			capacity:    d.node.Capacity,
			tierIndexes: d.node.TierIndexes,
			addr:        d.addr,
			name:        d.name,
		}
	}
	storageRing.tierIndexToName = make([]string, len(sb.tierIndexToName))
	copy(storageRing.tierIndexToName, sb.tierIndexToName)
	return storageRing
}

// Since we want to be able to persist this information, we'll need to make the
// JSON translators for the StorageBuilder.

type storageBuilderJSON struct {
	// ring.Builder is serializable on its own but we'll want to zero out its
	// nodes when we persist and copy in new node references when loading so we
	// aren't storing duplicate information
	Builder         *ring.Builder
	Disks           []*BuilderDisk
	TierIndexToName []string
	// No need to store tierNameToIndex; we can rebuild that on load.
}

func (sb *StorageBuilder) MarshalJSON() ([]byte, error) {
	savedBuilderNodes := sb.builder.Nodes
	sb.builder.Nodes = nil
	b, err := json.Marshal(&storageBuilderJSON{
		Builder:         &sb.builder,
		Disks:           sb.disks,
		TierIndexToName: sb.tierIndexToName,
	})
	sb.builder.Nodes = savedBuilderNodes
	return b, err
}

func (sb *StorageBuilder) UnmarshalJSON(b []byte) error {
	var j storageBuilderJSON
	if err := json.Unmarshal(b, &j); err != nil {
		return err
	}
	sb.builder = *j.Builder
	sb.disks = j.Disks
	sb.builder.Nodes = make([]*ring.Node, len(sb.disks))
	for i, bd := range sb.disks {
		sb.builder.Nodes[i] = &bd.node
	}
	sb.tierIndexToName = j.TierIndexToName
	sb.tierNameToIndex = make(map[string]uint32, len(sb.tierIndexToName))
	for ti, name := range sb.tierIndexToName {
		sb.tierNameToIndex[name] = uint32(ti)
	}
	return nil
}

// Now let's define the StorageDisk and StorageRing.
// These are completely immutable structs, as you don't want users of the ring
// to have to constantly check if things moved, replica counts changed, etc.
// Instead, those sort of changes would be propagated by distributing a new
// ring.

type StorageDisk struct {
	storageRing *StorageRing
	disabled    bool
	capacity    uint32
	tierIndexes []uint32
	addr        string
	name        string
}

func (sd *StorageDisk) Disabled() bool {
	return sd.disabled
}

func (sd *StorageDisk) Capacity() uint32 {
	return sd.capacity
}

func (sd *StorageDisk) Tiers() []string {
	tiers := make([]string, len(sd.tierIndexes))
	for i, ti := range sd.tierIndexes {
		tiers[i] = sd.storageRing.tierIndexToName[ti]
	}
	return tiers
}

func (sd *StorageDisk) Addr() string {
	return sd.addr
}

func (sd *StorageDisk) Name() string {
	return sd.name
}

type StorageRing struct {
	ring            ring.Ring
	disks           []*StorageDisk
	tierIndexToName []string
}

// This is the main lookup method. You give it an object name and it gives you
// the disks you should store it on.
// We're going to use fnv for the hashing here, for convenience, but you'd
// probably pick your favorite here, something better like blake2.

func (sr *StorageRing) DisksFor(objectName string) []*StorageDisk {
	hasher := fnv.New64a()
	hasher.Write([]byte(objectName))
	partition := hasher.Sum64() % uint64(sr.ring.PartitionCount())
	disks := make([]*StorageDisk, sr.ring.ReplicaCount())
	for replica := sr.ring.ReplicaCount() - 1; replica >= 0; replica-- {
		disks[replica] = sr.disks[sr.ring[replica][partition]]
	}
	return disks
}

// You would normally provide the JSON marshaling and unmarshaling methods for
// StorageRing and StorageDisk too, but we'll skip those for now.

// Now let's actually use all this stuff.

func Example_storageUseCase() {
	sb := &StorageBuilder{}
	sb.ChangeReplicaCount(3)
	// We're going to add a bunch of disks, two per server, four servers per
	// zone, and five zones.
	serverNumber := 0
	for _, zone := range []string{"ZoneA", "ZoneB", "ZoneC", "ZoneD", "ZoneE"} {
		for i := 0; i < 4; i++ {
			serverNumber++
			for _, disk := range []string{"sda1", "sdb1"} {
				bd := sb.AddDisk()
				// We're going to vary the capacities for a bit more work on
				// the rebalancer. This would usually represent the amount of
				// space, say, in gigabytes, that each disk has.
				bd.SetCapacity(uint32(100 + 100*i))
				bd.SetName(disk)
				bd.SetAddr(fmt.Sprintf("10.1.1.%d", serverNumber))
				bd.SetTier(0, fmt.Sprintf("Server%d", serverNumber))
				bd.SetTier(1, zone)
			}
		}
	}
	// Let's do a quick test of the JSON marshaling and unmarshaling.
	// We'll marshal the builder, unmarshal it into a new variable and
	// remarshal that, and compare.
	var first []byte
	var err error
	if first, err = json.Marshal(sb); err != nil {
		fmt.Println(err)
		return
	}
	sb2 := &StorageBuilder{}
	json.Unmarshal(first, &sb2)
	var second []byte
	if second, err = json.Marshal(sb2); err != nil {
		fmt.Println(err)
		return
	}
	if !bytes.Equal(first, second) {
		fmt.Println("not equal")
	}
	// Now let's actually get a useful ring and look up an object.
	sr := sb.StorageRing()
	objectName := "my test object"
	for replica, disk := range sr.DisksFor(objectName) {
		fmt.Printf("Replica %d of %q is on %s/%s which is on %s in %s\n", replica, objectName, disk.addr, disk.name, disk.Tiers()[0], disk.Tiers()[1])
	}
	// Output:
	// Replica 0 of "my test object" is on 10.1.1.2/sda1 which is on Server2 in ZoneA
	// Replica 1 of "my test object" is on 10.1.1.20/sdb1 which is on Server20 in ZoneE
	// Replica 2 of "my test object" is on 10.1.1.16/sda1 which is on Server16 in ZoneD
}
<commit_msg>Added the example output as a comment since godoc.org just removed it otherwise<commit_after>package ring_test

import (
	"bytes"
	"encoding/json"
	"fmt"
	"hash/fnv"

	"github.com/gholt/ring"
)

// This will be an in-depth implementation of using the ring package. We will
// be building a distributed object storage system where object names will be
// mapped to disks. There will be multiple disks per server, and multiple
// servers per zone.

// First, let's define our BuilderDisk, representing a single disk in the
// cluster and is what the ring package will be mapping assignments to.

// We want all the fields to be private because we don't want users of our new
// package to accidentally alter anything. This complicates our code, but
// simplifies things for users of our new storage package, and would be a
// common use case.

type BuilderDisk struct {
	// We'll define our specific StorageBuilder later. We need a reference to
	// it in each node so we notify it of changes and to resolve things like
	// tier names.
	storageBuilder *StorageBuilder
	// This is the actual ring.Node the ring.Builder will need to work with,
	// and contains whether the disk is Disabled or not, the Capacity, and the
	// list of indexes that define what tiers (server, zone) the disk is in.
	node ring.Node
	// The ip:port where the disk can be reached.
	addr string
	// The name of the disk on the server.
	name string
}

// Now we map public methods to the fields.

func (bd *BuilderDisk) Disabled() bool {
	return bd.node.Disabled
}

func (bd *BuilderDisk) SetDisabled(v bool) {
	bd.node.Disabled = v
}

func (bd *BuilderDisk) Capacity() uint32 {
	return bd.node.Capacity
}

func (bd *BuilderDisk) SetCapacity(v uint32) {
	bd.node.Capacity = v
}

func (bd *BuilderDisk) Tiers() []string {
	tiers := make([]string, len(bd.node.TierIndexes))
	for i, ti := range bd.node.TierIndexes {
		tiers[i] = bd.storageBuilder.tierIndexToName[ti]
	}
	return tiers
}

func (bd *BuilderDisk) SetTier(tier int, name string) {
	for tier >= len(bd.node.TierIndexes) {
		bd.node.TierIndexes = append(bd.node.TierIndexes, 0)
	}
	if bd.storageBuilder.tierNameToIndex == nil {
		bd.storageBuilder.tierNameToIndex = map[string]uint32{}
	}
	if ti, ok := bd.storageBuilder.tierNameToIndex[name]; ok {
		bd.node.TierIndexes[tier] = ti
	} else {
		ti = uint32(len(bd.storageBuilder.tierIndexToName))
		bd.storageBuilder.tierIndexToName = append(bd.storageBuilder.tierIndexToName, name)
		bd.storageBuilder.tierNameToIndex[name] = ti
		bd.node.TierIndexes[tier] = ti
	}
}

func (bd *BuilderDisk) Addr() string {
	return bd.addr
}

func (bd *BuilderDisk) SetAddr(v string) {
	bd.addr = v
}

func (bd *BuilderDisk) Name() string {
	return bd.name
}

func (bd *BuilderDisk) SetName(v string) {
	bd.name = v
}

// We want to be able to persist this information, so we make JSON translators.
// Note that this does not set the storageBuilder field; that has to be done by
// the StorageBuilder JSON translators.

type builderDiskJSON struct {
	// ring.Node is serializable on its own.
	Node *ring.Node
	Addr string
	Name string
}

func (bd *BuilderDisk) MarshalJSON() ([]byte, error) {
	return json.Marshal(&builderDiskJSON{
		Node: &bd.node,
		Addr: bd.addr,
		Name: bd.name,
	})
}

func (bd *BuilderDisk) UnmarshalJSON(b []byte) error {
	var j builderDiskJSON
	if err := json.Unmarshal(b, &j); err != nil {
		return err
	}
	bd.node = *j.Node
	bd.addr = j.Addr
	bd.name = j.Name
	return nil
}

// Now, let's define the StorageBuilder.

type StorageBuilder struct {
	// This is the actual builder that will do all the reblancing work.
	builder ring.Builder
	// These are all the disks in the cluster, we have to map them to the
	// ring.Builder's nodes.
	disks []*BuilderDisk
	// These are to give the tiers human readable names, like Server34 and
	// Zone5 or whatever is desired.
	tierIndexToName []string
	tierNameToIndex map[string]uint32
}

func (sb *StorageBuilder) AddDisk() *BuilderDisk {
	bd := &BuilderDisk{storageBuilder: sb}
	sb.disks = append(sb.disks, bd)
	sb.builder.Nodes = append(sb.builder.Nodes, &bd.node)
	return bd
}

func (sb *StorageBuilder) ChangeReplicaCount(count int) {
	sb.builder.ChangeReplicaCount(count)
}

// We'll skip removing disks, listing all disks, reading the replica count,
// partition count, changing last moved, etc. but those kind of methods would
// normally exist. The important point is that any modifications need to keep
// the ring.Builder's nodes in sync.

// We do want to give a useful ring to use, so let's provide that method. We'll
// define the StorageRing later, but it will be an immutable copy of the state
// of things at the time of StorageRing() call.

func (sb *StorageBuilder) StorageRing() *StorageRing {
	sb.builder.Rebalance()
	storageRing := &StorageRing{ring: sb.builder.RingDuplicate()}
	storageRing.disks = make([]*StorageDisk, len(sb.disks))
	for i, d := range sb.disks {
		storageRing.disks[i] = &StorageDisk{
			storageRing: storageRing,
			disabled:    d.node.Disabled,
			capacity:    d.node.Capacity,
			tierIndexes: d.node.TierIndexes,
			addr:        d.addr,
			name:        d.name,
		}
	}
	storageRing.tierIndexToName = make([]string, len(sb.tierIndexToName))
	copy(storageRing.tierIndexToName, sb.tierIndexToName)
	return storageRing
}

// Since we want to be able to persist this information, we'll need to make the
// JSON translators for the StorageBuilder.

type storageBuilderJSON struct {
	// ring.Builder is serializable on its own but we'll want to zero out its
	// nodes when we persist and copy in new node references when loading so we
	// aren't storing duplicate information
	Builder         *ring.Builder
	Disks           []*BuilderDisk
	TierIndexToName []string
	// No need to store tierNameToIndex; we can rebuild that on load.
}

func (sb *StorageBuilder) MarshalJSON() ([]byte, error) {
	savedBuilderNodes := sb.builder.Nodes
	sb.builder.Nodes = nil
	b, err := json.Marshal(&storageBuilderJSON{
		Builder:         &sb.builder,
		Disks:           sb.disks,
		TierIndexToName: sb.tierIndexToName,
	})
	sb.builder.Nodes = savedBuilderNodes
	return b, err
}

func (sb *StorageBuilder) UnmarshalJSON(b []byte) error {
	var j storageBuilderJSON
	if err := json.Unmarshal(b, &j); err != nil {
		return err
	}
	sb.builder = *j.Builder
	sb.disks = j.Disks
	sb.builder.Nodes = make([]*ring.Node, len(sb.disks))
	for i, bd := range sb.disks {
		sb.builder.Nodes[i] = &bd.node
	}
	sb.tierIndexToName = j.TierIndexToName
	sb.tierNameToIndex = make(map[string]uint32, len(sb.tierIndexToName))
	for ti, name := range sb.tierIndexToName {
		sb.tierNameToIndex[name] = uint32(ti)
	}
	return nil
}

// Now let's define the StorageDisk and StorageRing.
// These are completely immutable structs, as you don't want users of the ring
// to have to constantly check if things moved, replica counts changed, etc.
// Instead, those sort of changes would be propagated by distributing a new
// ring.

type StorageDisk struct {
	storageRing *StorageRing
	disabled    bool
	capacity    uint32
	tierIndexes []uint32
	addr        string
	name        string
}

func (sd *StorageDisk) Disabled() bool {
	return sd.disabled
}

func (sd *StorageDisk) Capacity() uint32 {
	return sd.capacity
}

func (sd *StorageDisk) Tiers() []string {
	tiers := make([]string, len(sd.tierIndexes))
	for i, ti := range sd.tierIndexes {
		tiers[i] = sd.storageRing.tierIndexToName[ti]
	}
	return tiers
}

func (sd *StorageDisk) Addr() string {
	return sd.addr
}

func (sd *StorageDisk) Name() string {
	return sd.name
}

type StorageRing struct {
	ring            ring.Ring
	disks           []*StorageDisk
	tierIndexToName []string
}

// This is the main lookup method. You give it an object name and it gives you
// the disks you should store it on.
// We're going to use fnv for the hashing here, for convenience, but you'd
// probably pick your favorite here, something better like blake2.

func (sr *StorageRing) DisksFor(objectName string) []*StorageDisk {
	hasher := fnv.New64a()
	hasher.Write([]byte(objectName))
	partition := hasher.Sum64() % uint64(sr.ring.PartitionCount())
	disks := make([]*StorageDisk, sr.ring.ReplicaCount())
	for replica := sr.ring.ReplicaCount() - 1; replica >= 0; replica-- {
		disks[replica] = sr.disks[sr.ring[replica][partition]]
	}
	return disks
}

// You would normally provide the JSON marshaling and unmarshaling methods for
// StorageRing and StorageDisk too, but we'll skip those for now.

// Now let's actually use all this stuff.

func Example_storageUseCase() {
	sb := &StorageBuilder{}
	sb.ChangeReplicaCount(3)
	// We're going to add a bunch of disks, two per server, four servers per
	// zone, and five zones.
	serverNumber := 0
	for _, zone := range []string{"ZoneA", "ZoneB", "ZoneC", "ZoneD", "ZoneE"} {
		for i := 0; i < 4; i++ {
			serverNumber++
			for _, disk := range []string{"sda1", "sdb1"} {
				bd := sb.AddDisk()
				// We're going to vary the capacities for a bit more work on
				// the rebalancer. This would usually represent the amount of
				// space, say, in gigabytes, that each disk has.
				bd.SetCapacity(uint32(100 + 100*i))
				bd.SetName(disk)
				bd.SetAddr(fmt.Sprintf("10.1.1.%d", serverNumber))
				bd.SetTier(0, fmt.Sprintf("Server%d", serverNumber))
				bd.SetTier(1, zone)
			}
		}
	}
	// Let's do a quick test of the JSON marshaling and unmarshaling.
	// We'll marshal the builder, unmarshal it into a new variable and
	// remarshal that, and compare.
	var first []byte
	var err error
	if first, err = json.Marshal(sb); err != nil {
		fmt.Println(err)
		return
	}
	sb2 := &StorageBuilder{}
	json.Unmarshal(first, &sb2)
	var second []byte
	if second, err = json.Marshal(sb2); err != nil {
		fmt.Println(err)
		return
	}
	if !bytes.Equal(first, second) {
		fmt.Println("not equal")
	}
	// Now let's actually get a useful ring and look up an object.
	sr := sb.StorageRing()
	objectName := "my test object"
	for replica, disk := range sr.DisksFor(objectName) {
		fmt.Printf("Replica %d of %q is on %s/%s which is on %s in %s\n", replica, objectName, disk.addr, disk.name, disk.Tiers()[0], disk.Tiers()[1])
	}
	// Replica 0 of "my test object" is on 10.1.1.2/sda1 which is on Server2 in ZoneA
	// Replica 1 of "my test object" is on 10.1.1.20/sdb1 which is on Server20 in ZoneE
	// Replica 2 of "my test object" is on 10.1.1.16/sda1 which is on Server16 in ZoneD
	// Output:
	// Replica 0 of "my test object" is on 10.1.1.2/sda1 which is on Server2 in ZoneA
	// Replica 1 of "my test object" is on 10.1.1.20/sdb1 which is on Server20 in ZoneE
	// Replica 2 of "my test object" is on 10.1.1.16/sda1 which is on Server16 in ZoneD
}
<|endoftext|>


<commit_before>//
// DISCLAIMER
//
// Copyright 2020 ArangoDB GmbH, Cologne, Germany
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Copyright holder is ArangoDB GmbH, Cologne, Germany
//

package test

import (
	"context"
	"testing"

	"github.com/arangodb/go-driver"
	"github.com/stretchr/testify/require"
)

// Test_Graph_AdvancedCreateV2 will check if graph created have properly set replication factor and write concern
func Test_Graph_AdvancedCreateV2(t *testing.T) {
	// Arrange
	ctx := context.Background()

	c := createClientFromEnv(t, true)
	v, err := c.Version(nil)
	require.NoError(t, err)

	skipNoCluster(c, t)

	db := ensureDatabase(ctx, c, databaseName("graph", "create", "replication"), nil, t)

	// Create
	graphID := db.Name() + "_graph"

	options, collections := newGraphOpts(db)
	options.ReplicationFactor = 3
	options.WriteConcern = 2

	_, err = db.CreateGraphV2(ctx, graphID, &options)
	require.NoError(t, err)

	// Wait for collections to be created
	waitForCollections(t, db, collections)

	t.Run("Ensure all properties are set properly", func(t *testing.T) {
		for _, collName := range collections {
			collection, err := db.Collection(ctx, collName)
			require.NoError(t, err)

			prop, err := collection.Properties(ctx)
			require.NoError(t, err)

			require.Equalf(t, 3, prop.NumberOfShards, "NumberOfShards mismatch for %s", collName)

			require.Equalf(t, 3, prop.ReplicationFactor, "ReplicationFactor mismatch for %s", collName)
			if v.Version.CompareTo("3.6") >= 0 {
				require.Equalf(t, 2, prop.WriteConcern, "WriteConcern mismatch for %s", collName)
			}
		}
	})
}

// Test_Graph_AdvancedCreateV2_Defaults will check if graph created have properly set replication factor and write concern by default
func Test_Graph_AdvancedCreateV2_Defaults(t *testing.T) {
	// Arrange
	ctx := context.Background()

	c := createClientFromEnv(t, true)
	v, err := c.Version(nil)
	require.NoError(t, err)

	skipNoCluster(c, t)

	db := ensureDatabase(ctx, c, databaseName("graph", "create", "defaults"), nil, t)

	// Create
	graphID := db.Name() + "_graph"

	options, collections := newGraphOpts(db)

	_, err = db.CreateGraphV2(ctx, graphID, &options)
	require.NoError(t, err)

	// Wait for collections to be created
	waitForCollections(t, db, collections)

	t.Run("Ensure all properties are set properly by default", func(t *testing.T) {
		for _, collName := range collections {
			collection, err := db.Collection(ctx, collName)
			require.NoError(t, err)

			prop, err := collection.Properties(ctx)
			require.NoError(t, err)

			require.Equalf(t, 1, prop.ReplicationFactor, "ReplicationFactor mismatch for %s", collName)
			if v.Version.CompareTo("3.6") >= 0 {
				require.Equalf(t, 1, prop.WriteConcern, "WriteConcern mismatch for %s", collName)
			}
		}
	})
}

func TestGraphCreationV2(t *testing.T) {
	// Arrange
	ctx := context.Background()

	c := createClientFromEnv(t, true)
	EnsureVersion(t, ctx, c).CheckVersion(MinimumVersion("3.7.0")).Cluster().Enterprise()

	t.Run("Satellite", func(t *testing.T) {
		db := ensureDatabase(ctx, c, databaseName("graph", "create", "defaults"), nil, t)

		// Create
		graphID := db.Name() + "_graph"

		options, collections := newGraphOpts(db)

		options.ReplicationFactor = driver.SatelliteGraph
		options.IsSmart = false
		options.SmartGraphAttribute = ""

		g, err := db.CreateGraphV2(ctx, graphID, &options)
		require.NoError(t, err)

		// Wait for collections to be created
		waitForCollections(t, db, collections)

		require.True(t, g.IsSatellite())
	})

	t.Run("Satellite - list", func(t *testing.T) {
		db := ensureDatabase(ctx, c, databaseName("graph", "create", "defaults"), nil, t)

		// Create
		graphID := db.Name() + "_graph"

		options, collections := newGraphOpts(db)

		options.ReplicationFactor = driver.SatelliteGraph
		options.IsSmart = false
		options.SmartGraphAttribute = ""

		g, err := db.CreateGraphV2(ctx, graphID, &options)
		require.NoError(t, err)

		// Wait for collections to be created
		waitForCollections(t, db, collections)

		graphs, err := db.Graphs(ctx)
		require.NoError(t, err)
		require.Len(t, graphs, 1)

		require.Equal(t, g.Name(), graphs[0].Name())
		require.True(t, graphs[0].IsSatellite())
	})

	t.Run("Standard", func(t *testing.T) {
		db := ensureDatabase(ctx, c, databaseName("graph", "create", "defaults"), nil, t)

		// Create
		graphID := db.Name() + "_graph"

		options, collections := newGraphOpts(db)

		g, err := db.CreateGraphV2(ctx, graphID, &options)
		require.NoError(t, err)

		// Wait for collections to be created
		waitForCollections(t, db, collections)

		require.False(t, g.IsSatellite())
	})

	t.Run("Standard - list", func(t *testing.T) {
		db := ensureDatabase(ctx, c, databaseName("graph", "create", "defaults"), nil, t)

		// Create
		graphID := db.Name() + "_graph"

		options, collections := newGraphOpts(db)

		g, err := db.CreateGraphV2(ctx, graphID, &options)
		require.NoError(t, err)

		// Wait for collections to be created
		waitForCollections(t, db, collections)

		graphs, err := db.Graphs(ctx)
		require.NoError(t, err)
		require.Len(t, graphs, 1)

		require.Equal(t, g.Name(), graphs[0].Name())
		require.False(t, graphs[0].IsSatellite())
	})

	t.Run("Disjoint", func(t *testing.T) {
		db := ensureDatabase(ctx, c, databaseName("graph", "create", "defaults"), nil, t)

		// Create
		graphID := db.Name() + "_graph"

		options, collections := newGraphOpts(db)

		options.IsDisjoint = true

		g, err := db.CreateGraphV2(ctx, graphID, &options)
		require.NoError(t, err)

		// Wait for collections to be created
		waitForCollections(t, db, collections)

		require.True(t, g.IsDisjoint())
	})

	t.Run("Disjoint - list", func(t *testing.T) {
		db := ensureDatabase(ctx, c, databaseName("graph", "create", "defaults"), nil, t)

		// Create
		graphID := db.Name() + "_graph"

		options, collections := newGraphOpts(db)

		options.IsDisjoint = true

		g, err := db.CreateGraphV2(ctx, graphID, &options)
		require.NoError(t, err)

		// Wait for collections to be created
		waitForCollections(t, db, collections)

		graphs, err := db.Graphs(ctx)
		require.NoError(t, err)
		require.Len(t, graphs, 1)

		require.Equal(t, g.Name(), graphs[0].Name())
		require.True(t, graphs[0].IsDisjoint())
	})
}

func TestHybridSmartGraphCreationV2(t *testing.T) {
	ctx := context.Background()

	c := createClientFromEnv(t, true)
	EnsureVersion(t, ctx, c).CheckVersion(MinimumVersion("3.9.0")).Cluster().Enterprise()

	db := ensureDatabase(ctx, c, databaseName("graph", "create", "hybrid"), nil, t)

	name := db.Name() + "_test_create_hybrid_graph"
	colName := db.Name() + "_create_hybrid_edge_col"
	col1Name := db.Name() + "_sat_edge_col"
	col2Name := db.Name() + "_non_sat_edge_col"

	options := driver.CreateGraphOptions{
		IsSmart:             true,
		SmartGraphAttribute: "test",
		ReplicationFactor:   2,
		NumberOfShards:      2,
		Satellites:          []string{colName, col1Name},
		EdgeDefinitions: []driver.EdgeDefinition{{
			Collection: colName,
			From:       []string{col1Name},
			To:         []string{col2Name},
		}},
	}
	g, err := db.CreateGraphV2(ctx, name, &options)
	if err != nil {
		t.Fatalf("Failed to create graph '%s': %s", name, describe(err))
	}

	graphs, err := db.Graphs(ctx)
	require.NoError(t, err)
	require.Len(t, graphs, 1)

	require.Equal(t, g.Name(), graphs[0].Name())
	require.True(t, graphs[0].IsSmart())

	for _, collName := range []string{colName, col1Name, col2Name} {
		collection, err := db.Collection(ctx, collName)
		require.NoError(t, err)

		prop, err := collection.Properties(ctx)
		require.NoError(t, err)

		if collName == col2Name {
			require.Equalf(t, 2, prop.ReplicationFactor, "ReplicationFactor mismatch for %s", collName)
			require.Equalf(t, 2, prop.NumberOfShards, "NumberOfShards mismatch for %s", collName)
		} else {
			require.True(t, prop.IsSatellite())
		}
	}
}
<commit_msg>[TG-209] Adjust Graph tests for the latest changes in 3.9 (#360)<commit_after>//
// DISCLAIMER
//
// Copyright 2020 ArangoDB GmbH, Cologne, Germany
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Copyright holder is ArangoDB GmbH, Cologne, Germany
//

package test

import (
	"context"
	"testing"

	"github.com/arangodb/go-driver"
	"github.com/stretchr/testify/require"
)

// Test_Graph_AdvancedCreateV2 will check if graph created have properly set replication factor and write concern
func Test_Graph_AdvancedCreateV2(t *testing.T) {
	// Arrange
	ctx := context.Background()

	c := createClientFromEnv(t, true)
	v, err := c.Version(nil)
	require.NoError(t, err)

	skipNoCluster(c, t)

	db := ensureDatabase(ctx, c, databaseName("graph", "create", "replication"), nil, t)

	// Create
	graphID := db.Name() + "_graph"

	options, collections := newGraphOpts(db)
	options.ReplicationFactor = 3
	options.WriteConcern = 2

	_, err = db.CreateGraphV2(ctx, graphID, &options)
	require.NoError(t, err)

	// Wait for collections to be created
	waitForCollections(t, db, collections)

	t.Run("Ensure all properties are set properly", func(t *testing.T) {
		for _, collName := range collections {
			collection, err := db.Collection(ctx, collName)
			require.NoError(t, err)

			prop, err := collection.Properties(ctx)
			require.NoError(t, err)

			require.Equalf(t, 3, prop.NumberOfShards, "NumberOfShards mismatch for %s", collName)

			require.Equalf(t, 3, prop.ReplicationFactor, "ReplicationFactor mismatch for %s", collName)
			if v.Version.CompareTo("3.6") >= 0 {
				require.Equalf(t, 2, prop.WriteConcern, "WriteConcern mismatch for %s", collName)
			}
		}
	})
}

// Test_Graph_AdvancedCreateV2_Defaults will check if graph created have properly set replication factor and write concern by default
func Test_Graph_AdvancedCreateV2_Defaults(t *testing.T) {
	// Arrange
	ctx := context.Background()

	c := createClientFromEnv(t, true)
	v, err := c.Version(nil)
	require.NoError(t, err)

	skipNoCluster(c, t)

	db := ensureDatabase(ctx, c, databaseName("graph", "create", "defaults"), nil, t)

	// Create
	graphID := db.Name() + "_graph"

	options, collections := newGraphOpts(db)

	_, err = db.CreateGraphV2(ctx, graphID, &options)
	require.NoError(t, err)

	// Wait for collections to be created
	waitForCollections(t, db, collections)

	t.Run("Ensure all properties are set properly by default", func(t *testing.T) {
		for _, collName := range collections {
			collection, err := db.Collection(ctx, collName)
			require.NoError(t, err)

			prop, err := collection.Properties(ctx)
			require.NoError(t, err)

			require.Equalf(t, 1, prop.ReplicationFactor, "ReplicationFactor mismatch for %s", collName)
			if v.Version.CompareTo("3.6") >= 0 {
				require.Equalf(t, 1, prop.WriteConcern, "WriteConcern mismatch for %s", collName)
			}
		}
	})
}

func TestGraphCreationV2(t *testing.T) {
	// Arrange
	ctx := context.Background()

	c := createClientFromEnv(t, true)
	EnsureVersion(t, ctx, c).CheckVersion(MinimumVersion("3.7.0")).Cluster().Enterprise()

	t.Run("Satellite", func(t *testing.T) {
		db := ensureDatabase(ctx, c, databaseName("graph", "create", "defaults"), nil, t)

		// Create
		graphID := db.Name() + "_graph"

		options, collections := newGraphOpts(db)

		options.ReplicationFactor = driver.SatelliteGraph
		options.NumberOfShards = 1
		options.IsSmart = false
		options.SmartGraphAttribute = ""

		g, err := db.CreateGraphV2(ctx, graphID, &options)
		require.NoError(t, err)

		// Wait for collections to be created
		waitForCollections(t, db, collections)

		require.True(t, g.IsSatellite())
	})

	t.Run("Satellite - list", func(t *testing.T) {
		db := ensureDatabase(ctx, c, databaseName("graph", "create", "defaults"), nil, t)

		// Create
		graphID := db.Name() + "_graph"

		options, collections := newGraphOpts(db)

		options.ReplicationFactor = driver.SatelliteGraph
		options.NumberOfShards = 1
		options.IsSmart = false
		options.SmartGraphAttribute = ""

		g, err := db.CreateGraphV2(ctx, graphID, &options)
		require.NoError(t, err)

		// Wait for collections to be created
		waitForCollections(t, db, collections)

		graphs, err := db.Graphs(ctx)
		require.NoError(t, err)
		require.Len(t, graphs, 1)

		require.Equal(t, g.Name(), graphs[0].Name())
		require.True(t, graphs[0].IsSatellite())
	})

	t.Run("Standard", func(t *testing.T) {
		db := ensureDatabase(ctx, c, databaseName("graph", "create", "defaults"), nil, t)

		// Create
		graphID := db.Name() + "_graph"

		options, collections := newGraphOpts(db)

		g, err := db.CreateGraphV2(ctx, graphID, &options)
		require.NoError(t, err)

		// Wait for collections to be created
		waitForCollections(t, db, collections)

		require.False(t, g.IsSatellite())
	})

	t.Run("Standard - list", func(t *testing.T) {
		db := ensureDatabase(ctx, c, databaseName("graph", "create", "defaults"), nil, t)

		// Create
		graphID := db.Name() + "_graph"

		options, collections := newGraphOpts(db)

		g, err := db.CreateGraphV2(ctx, graphID, &options)
		require.NoError(t, err)

		// Wait for collections to be created
		waitForCollections(t, db, collections)

		graphs, err := db.Graphs(ctx)
		require.NoError(t, err)
		require.Len(t, graphs, 1)

		require.Equal(t, g.Name(), graphs[0].Name())
		require.False(t, graphs[0].IsSatellite())
	})

	t.Run("Disjoint", func(t *testing.T) {
		db := ensureDatabase(ctx, c, databaseName("graph", "create", "defaults"), nil, t)

		// Create
		graphID := db.Name() + "_graph"

		options, collections := newGraphOpts(db)

		options.IsDisjoint = true

		g, err := db.CreateGraphV2(ctx, graphID, &options)
		require.NoError(t, err)

		// Wait for collections to be created
		waitForCollections(t, db, collections)

		require.True(t, g.IsDisjoint())
	})

	t.Run("Disjoint - list", func(t *testing.T) {
		db := ensureDatabase(ctx, c, databaseName("graph", "create", "defaults"), nil, t)

		// Create
		graphID := db.Name() + "_graph"

		options, collections := newGraphOpts(db)

		options.IsDisjoint = true

		g, err := db.CreateGraphV2(ctx, graphID, &options)
		require.NoError(t, err)

		// Wait for collections to be created
		waitForCollections(t, db, collections)

		graphs, err := db.Graphs(ctx)
		require.NoError(t, err)
		require.Len(t, graphs, 1)

		require.Equal(t, g.Name(), graphs[0].Name())
		require.True(t, graphs[0].IsDisjoint())
	})
}

func TestHybridSmartGraphCreationV2(t *testing.T) {
	ctx := context.Background()

	c := createClientFromEnv(t, true)
	EnsureVersion(t, ctx, c).CheckVersion(MinimumVersion("3.9.0")).Cluster().Enterprise()

	db := ensureDatabase(ctx, c, databaseName("graph", "create", "hybrid"), nil, t)

	name := db.Name() + "_test_create_hybrid_graph"
	colName := db.Name() + "_create_hybrid_edge_col"
	col1Name := db.Name() + "_sat_edge_col"
	col2Name := db.Name() + "_non_sat_edge_col"

	options := driver.CreateGraphOptions{
		IsSmart:             true,
		SmartGraphAttribute: "test",
		ReplicationFactor:   2,
		NumberOfShards:      2,
		Satellites:          []string{colName, col1Name},
		EdgeDefinitions: []driver.EdgeDefinition{{
			Collection: colName,
			From:       []string{col1Name},
			To:         []string{col2Name},
		}},
	}
	g, err := db.CreateGraphV2(ctx, name, &options)
	if err != nil {
		t.Fatalf("Failed to create graph '%s': %s", name, describe(err))
	}

	graphs, err := db.Graphs(ctx)
	require.NoError(t, err)
	require.Len(t, graphs, 1)

	require.Equal(t, g.Name(), graphs[0].Name())
	require.True(t, graphs[0].IsSmart())

	for _, collName := range []string{colName, col1Name, col2Name} {
		collection, err := db.Collection(ctx, collName)
		require.NoError(t, err)

		prop, err := collection.Properties(ctx)
		require.NoError(t, err)

		if collName == col2Name {
			require.Equalf(t, 2, prop.ReplicationFactor, "ReplicationFactor mismatch for %s", collName)
			require.Equalf(t, 2, prop.NumberOfShards, "NumberOfShards mismatch for %s", collName)
		} else {
			require.True(t, prop.IsSatellite())
		}
	}
}
<|endoftext|>