feat(group): add 'madb group rename' command

Change-Id: I67e1ac2c9cacfc2714a1655e0b2f1d60daf9333a
diff --git a/doc.go b/doc.go
index 31b6310..d60a6f3 100644
--- a/doc.go
+++ b/doc.go
@@ -233,6 +233,7 @@
 The madb group commands are:
    add         Add members to a device group
    remove      Remove members from a device group
+   rename      Rename an existing device group
 
 Madb group add - Add members to a device group
 
@@ -269,6 +270,19 @@
 <member> is a member specifier, which can be one of device serial, qualifier,
 device index (e.g., '@1', '@2'), device nickname, or another device group.
 
+Madb group rename - Rename an existing device group
+
+Renames an existing device group.
+
+Usage:
+   madb group rename [flags] <old_name> <new_name>
+
+<old_name> is the name of an existing device group.
+
+<new_name> is the new name for the existing group. This must be an alpha-numeric
+string with no special characters or spaces, and must not conflict with another
+existing device or group name.
+
 Madb install - Install your app on all devices
 
 Installs your app on all devices.
diff --git a/group.go b/group.go
index dc1e563..2cbaf1d 100644
--- a/group.go
+++ b/group.go
@@ -13,7 +13,6 @@
 )
 
 // TODO(youngseokyoon): implement the following sub-commands.
-//  - rename:    rename a group
 //  - delete:    delete a group
 //  - list:      list all the groups and their members
 //  - clear-all: delete all the existing device groups
@@ -21,7 +20,11 @@
 // TODO(youngseokyoon): use the groups for filtering devices.
 
 var cmdMadbGroup = &cmdline.Command{
-	Children:         []*cmdline.Command{cmdMadbGroupAdd, cmdMadbGroupRemove},
+	Children: []*cmdline.Command{
+		cmdMadbGroupAdd,
+		cmdMadbGroupRemove,
+		cmdMadbGroupRename,
+	},
 	Name:             "group",
 	DontInheritFlags: true,
 	Short:            "Manage device groups",
@@ -140,6 +143,54 @@
 	return writeConfig(cfg, filename)
 }
 
+var cmdMadbGroupRename = &cmdline.Command{
+	Runner: subCommandRunnerWithFilepath{runMadbGroupRename, getDefaultConfigFilePath},
+	Name:   "rename",
+	Short:  "Rename an existing device group",
+	Long: `
+Renames an existing device group.
+`,
+	ArgsName: "<old_name> <new_name>",
+	ArgsLong: `
+<old_name> is the name of an existing device group.
+
+<new_name> is the new name for the existing group.
+This must be an alpha-numeric string with no special characters or spaces,
+and must not conflict with another existing device or group name.
+`,
+}
+
+func runMadbGroupRename(env *cmdline.Env, args []string, filename string) error {
+	// Check if the arguments are valid.
+	if len(args) != 2 {
+		return env.UsageErrorf("There must be exactly two arguments.")
+	}
+
+	oldName, newName := args[0], args[1]
+	if !isValidName(oldName) {
+		return fmt.Errorf("Not a valid group name: %q", oldName)
+	}
+	if !isValidName(newName) {
+		return fmt.Errorf("Not a valid group name: %q", newName)
+	}
+
+	cfg, err := readConfig(filename)
+	if err != nil {
+		return err
+	}
+	if !isGroupName(oldName, cfg) {
+		return fmt.Errorf("Not an existing group name: %q", oldName)
+	}
+	if isNameInUse(newName, cfg) {
+		return fmt.Errorf("The provided name is already in use: %q", newName)
+	}
+
+	cfg.Groups[newName] = cfg.Groups[oldName]
+	delete(cfg.Groups, oldName)
+
+	return writeConfig(cfg, filename)
+}
+
 // isValidMember takes a member string given as an argument, and returns nil
 // when the member string is valid. Otherwise, an error is returned inicating
 // the reason why the given member string is not valid.
diff --git a/group_test.go b/group_test.go
index a364714..23c235b 100644
--- a/group_test.go
+++ b/group_test.go
@@ -224,3 +224,101 @@
 
 	runGroupTests(t, tests)
 }
+
+func TestMadbGroupRename(t *testing.T) {
+	tests := []testSequence{
+		{
+			{
+				runMadbGroupRename,
+				[]string{},
+				map[string][]string{},
+				true,
+			},
+			{
+				runMadbGroupRename,
+				[]string{"GROUP1"},
+				map[string][]string{},
+				true,
+			},
+			{
+				runMadbGroupRename,
+				[]string{"GROUP1", "GROUP2"},
+				map[string][]string{},
+				true,
+			},
+		},
+		{
+			{
+				runMadbGroupAdd,
+				[]string{"GROUP1", "SERIAL1", "NICKNAME1", "NICKNAME2", "SERIAL2", "NICKNAME3"},
+				map[string][]string{"GROUP1": []string{"SERIAL1", "NICKNAME1", "NICKNAME2", "SERIAL2", "NICKNAME3"}},
+				false,
+			},
+			{
+				runMadbGroupAdd,
+				[]string{"GROUP2", "SERIAL3", "NICKNAME4"},
+				map[string][]string{
+					"GROUP1": []string{"SERIAL1", "NICKNAME1", "NICKNAME2", "SERIAL2", "NICKNAME3"},
+					"GROUP2": []string{"SERIAL3", "NICKNAME4"},
+				},
+				false,
+			},
+			{
+				runMadbGroupRename,
+				[]string{"GROUP1", "GROUP3"},
+				map[string][]string{
+					"GROUP2": []string{"SERIAL3", "NICKNAME4"},
+					"GROUP3": []string{"SERIAL1", "NICKNAME1", "NICKNAME2", "SERIAL2", "NICKNAME3"},
+				},
+				false,
+			},
+			{
+				runMadbGroupRename,
+				[]string{"GROUP2", "_!@#"},
+				map[string][]string{
+					"GROUP2": []string{"SERIAL3", "NICKNAME4"},
+					"GROUP3": []string{"SERIAL1", "NICKNAME1", "NICKNAME2", "SERIAL2", "NICKNAME3"},
+				},
+				true,
+			},
+			{
+				runMadbGroupRename,
+				[]string{"GROUP2", "GROUP3"},
+				map[string][]string{
+					"GROUP2": []string{"SERIAL3", "NICKNAME4"},
+					"GROUP3": []string{"SERIAL1", "NICKNAME1", "NICKNAME2", "SERIAL2", "NICKNAME3"},
+				},
+				true,
+			},
+		},
+	}
+
+	runGroupTests(t, tests)
+}
+
+func TestMadbGroupRenameNameConflict(t *testing.T) {
+	tests := []testSequence{
+		{
+			{
+				runMadbNameSet,
+				[]string{"SERIAL1", "NICKNAME1"},
+				map[string][]string{},
+				false,
+			},
+			{
+				runMadbGroupAdd,
+				[]string{"GROUP1", "SERIAL1"},
+				map[string][]string{"GROUP1": []string{"SERIAL1"}},
+				false,
+			},
+			{
+				runMadbGroupRename,
+				[]string{"GROUP1", "NICKNAME1"},
+				map[string][]string{"GROUP1": []string{"SERIAL1"}},
+				true,
+			},
+		},
+	}
+
+	runGroupTests(t, tests)
+}