refactor: merge "nicknames" and "users" files into a single "config" file.

Added a new "config" struct, which holds both the nickname data and
the default user IDs. The plan is to also use this config file to
store the device group definitions.

Change-Id: I1ea6615e3e47a6434cea1651dec23701164636e3
diff --git a/madb.go b/madb.go
index ca35d91..486a9ae 100644
--- a/madb.go
+++ b/madb.go
@@ -145,28 +145,23 @@
 }
 
 // Runs "adb devices -l" command, and parses the result to get all the device serial numbers.
-func getDevices(nicknameFile string, userFile string) ([]device, error) {
+func getDevices(configFile string) ([]device, error) {
 	sh := gosh.NewShell(nil)
 	defer sh.Cleanup()
 
 	output := sh.Cmd("adb", "devices", "-l").Stdout()
 
-	nicknameSerialMap, err := readMapFromFile(nicknameFile)
+	cfg, err := readConfig(configFile)
 	if err != nil {
 		return nil, err
 	}
 
-	serialUserMap, err := readMapFromFile(userFile)
-	if err != nil {
-		return nil, err
-	}
-
-	return parseDevicesOutput(output, nicknameSerialMap, serialUserMap)
+	return parseDevicesOutput(output, cfg)
 }
 
 // Parses the output generated from "adb devices -l" command and return the list of device serial numbers
 // Devices that are currently offline are excluded from the returned list.
-func parseDevicesOutput(output string, nicknameSerialMap map[string]string, serialUserMap map[string]string) ([]device, error) {
+func parseDevicesOutput(output string, cfg *config) ([]device, error) {
 	lines := strings.Split(output, "\n")
 
 	result := []device{}
@@ -198,26 +193,28 @@
 			d.Type = realDevice
 		}
 
-		// Determine whether there is a nickname defined for this device,
-		// so that the console output prefix can display the nickname instead of the serial.
-	NSMLoop:
-		for nickname, serial := range nicknameSerialMap {
-			if d.Serial == serial {
-				d.Nickname = nickname
-				break
-			}
-
-			for _, qualifier := range d.Qualifiers {
-				if qualifier == serial {
+		if cfg != nil {
+			// Determine whether there is a nickname defined for this device,
+			// so that the console output prefix can display the nickname instead of the serial.
+		NSMLoop:
+			for nickname, serial := range cfg.Names {
+				if d.Serial == serial {
 					d.Nickname = nickname
-					break NSMLoop
+					break
+				}
+
+				for _, qualifier := range d.Qualifiers {
+					if qualifier == serial {
+						d.Nickname = nickname
+						break NSMLoop
+					}
 				}
 			}
-		}
 
-		// Determine whether there is a default user ID set by 'madb user'.
-		if userID, ok := serialUserMap[d.Serial]; ok {
-			d.UserID = userID
+			// Determine whether there is a default user ID set by 'madb user'.
+			if userID, ok := cfg.UserIDs[d.Serial]; ok {
+				d.UserID = userID
+			}
 		}
 
 		result = append(result, d)
@@ -229,17 +226,12 @@
 // Gets all the devices specified by the device specifier flags.
 // Intended to be used by most of the madb sub-commands except for 'madb name'.
 func getSpecifiedDevices() ([]device, error) {
-	nicknameFile, err := getDefaultNameFilePath()
+	configFile, err := getDefaultConfigFilePath()
 	if err != nil {
 		return nil, err
 	}
 
-	userFile, err := getDefaultUserFilePath()
-	if err != nil {
-		return nil, err
-	}
-
-	allDevices, err := getDevices(nicknameFile, userFile)
+	allDevices, err := getDevices(configFile)
 	if err != nil {
 		return nil, err
 	}
@@ -336,6 +328,18 @@
 	return false
 }
 
+// config contains various configuration information for madb.
+type config struct {
+	// Version indicates the version string of madb binary by which this config
+	// was written to the file, in case it has to be migrated to a newer schema.
+	Version string
+	// Names keeps the mapping between device nicknames and their serials.
+	Names map[string]string
+	// UserIDs keeps the mapping between device serials and their default user
+	// IDs.
+	UserIDs map[string]string
+}
+
 // Returns the config dir located at "~/.madb"
 func getConfigDir() (string, error) {
 	home := os.Getenv("HOME")
@@ -351,6 +355,72 @@
 	return configDir, nil
 }
 
+// getDefaultConfigFilePath returns the default location of the config file.
+func getDefaultConfigFilePath() (string, error) {
+	configDir, err := getConfigDir()
+	if err != nil {
+		return "", err
+	}
+
+	return filepath.Join(configDir, "config"), nil
+}
+
+// readConfig reads the provided file and reconstructs the config struct.
+// When the file does not exist, it returns an empty config with the members
+// initialized as empty maps.
+func readConfig(filename string) (*config, error) {
+	result := new(config)
+
+	// The file may not exist or be empty when there are no stored data.
+	if stat, err := os.Stat(filename); os.IsNotExist(err) || (err == nil && stat.Size() == 0) {
+		result.Names = make(map[string]string)
+		result.UserIDs = make(map[string]string)
+		return result, nil
+	}
+
+	f, err := os.Open(filename)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+
+	decoder := json.NewDecoder(f)
+
+	// Decoding might fail when the file is somehow corrupted, or when the schema is updated.
+	// In such cases, move on after resetting the cache file instead of exiting the app.
+	if err := decoder.Decode(result); err != nil {
+		fmt.Fprintf(os.Stderr, "WARNING: Could not decode the file: %q. Resetting the file.\n", err)
+		if err := os.Remove(f.Name()); err != nil {
+			return nil, err
+		}
+
+		result = new(config)
+	}
+
+	if result.Names == nil {
+		result.Names = make(map[string]string)
+	}
+	if result.UserIDs == nil {
+		result.Names = make(map[string]string)
+	}
+
+	return result, nil
+}
+
+// writeConfig takes a config and writes it into the provided file name.
+func writeConfig(cfg *config, filename string) error {
+	f, err := os.Create(filename)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	cfg.Version = version
+
+	encoder := json.NewEncoder(f)
+	return encoder.Encode(*cfg)
+}
+
 type subCommandRunner struct {
 	// init is an optional function that does some initial work that should only
 	// be performed once, before directing the command to all the devices.
@@ -666,50 +736,6 @@
 	return result, nil
 }
 
-// readMapFromFile reads the provided file and reconstructs the string => string map.
-// When the file does not exist, it returns an empty map (instead of nil), so that callers can safely add new entries.
-func readMapFromFile(filename string) (map[string]string, error) {
-	result := make(map[string]string)
-
-	// The file may not exist or be empty when there are no stored data.
-	if stat, err := os.Stat(filename); os.IsNotExist(err) || (err == nil && stat.Size() == 0) {
-		return result, nil
-	}
-
-	f, err := os.Open(filename)
-	if err != nil {
-		return nil, err
-	}
-	defer f.Close()
-
-	decoder := json.NewDecoder(f)
-
-	// Decoding might fail when the file is somehow corrupted, or when the schema is updated.
-	// In such cases, move on after resetting the cache file instead of exiting the app.
-	if err := decoder.Decode(&result); err != nil {
-		fmt.Fprintf(os.Stderr, "WARNING: Could not decode the file: %q. Resetting the file.\n", err)
-		if err := os.Remove(f.Name()); err != nil {
-			return nil, err
-		}
-
-		return make(map[string]string), nil
-	}
-
-	return result, nil
-}
-
-// writeMapToFile takes a string => string map and writes it into the provided file name.
-func writeMapToFile(data map[string]string, filename string) error {
-	f, err := os.Create(filename)
-	if err != nil {
-		return err
-	}
-	defer f.Close()
-
-	encoder := json.NewEncoder(f)
-	return encoder.Encode(data)
-}
-
 // expandKeywords takes a command line argument and a device configuration, and returns a new
 // argument where the predefined keywords ("{{index}}", "{{name}}", "{{serial}}") are expanded.
 func expandKeywords(arg string, d device) string {
diff --git a/madb_test.go b/madb_test.go
index 0ef5843..395c1d5 100644
--- a/madb_test.go
+++ b/madb_test.go
@@ -41,7 +41,7 @@
 
 `
 
-	got, err := parseDevicesOutput(output, nil, nil)
+	got, err := parseDevicesOutput(output, nil)
 	if err != nil {
 		t.Fatalf("failed to parse the output: %v", err)
 	}
@@ -74,7 +74,7 @@
 
 `
 
-	got, err = parseDevicesOutput(output, nil, nil)
+	got, err = parseDevicesOutput(output, nil)
 	if err != nil {
 		t.Fatalf("failed to parse the output: %v", err)
 	}
@@ -88,7 +88,7 @@
 deviceid02       device product:sdk_phone_armv7 model:sdk_phone_armv7 device:generic
 
 `
-	got, err = parseDevicesOutput(output, nil, nil)
+	got, err = parseDevicesOutput(output, nil)
 	if err != nil {
 		t.Fatalf("failed to parse the output: %v", err)
 	}
@@ -115,16 +115,17 @@
 
 	`
 
-	nicknameSerialMap := map[string]string{
-		"MyPhone": "deviceid01",
-		"ARMv7":   "model:sdk_phone_armv7",
+	cfg := &config{
+		Names: map[string]string{
+			"MyPhone": "deviceid01",
+			"ARMv7":   "model:sdk_phone_armv7",
+		},
+		UserIDs: map[string]string{
+			"deviceid01": "10",
+		},
 	}
 
-	serialUserMap := map[string]string{
-		"deviceid01": "10",
-	}
-
-	got, err = parseDevicesOutput(output, nicknameSerialMap, serialUserMap)
+	got, err = parseDevicesOutput(output, cfg)
 	if err != nil {
 		t.Fatalf("failed to parse the output: %v", err)
 	}
diff --git a/name.go b/name.go
index 1da7f52..aca6661 100644
--- a/name.go
+++ b/name.go
@@ -6,8 +6,6 @@
 
 import (
 	"fmt"
-	"os"
-	"path/filepath"
 	"regexp"
 
 	"v.io/x/lib/cmdline"
@@ -25,7 +23,7 @@
 }
 
 var cmdMadbNameSet = &cmdline.Command{
-	Runner: subCommandRunnerWithFilepath{runMadbNameSet, getDefaultNameFilePath},
+	Runner: subCommandRunnerWithFilepath{runMadbNameSet, getDefaultConfigFilePath},
 	Name:   "set",
 	Short:  "Set a nickname to be used in place of the device serial.",
 	Long: `
@@ -75,30 +73,30 @@
 		return env.UsageErrorf("Not a valid nickname: %v", nickname)
 	}
 
-	nicknameSerialMap, err := readMapFromFile(filename)
+	cfg, err := readConfig(filename)
 	if err != nil {
 		return err
 	}
 
 	// If the nickname is already in use, don't allow it at all.
-	if _, present := nicknameSerialMap[nickname]; present {
+	if _, present := cfg.Names[nickname]; present {
 		return fmt.Errorf("The provided nickname %q is already in use.", nickname)
 	}
 
 	// If the serial number already has an assigned nickname, delete it first.
 	// Need to do this check, because the nickname-serial map should be a one-to-one mapping.
-	if nickname, present := reverseMap(nicknameSerialMap)[serial]; present {
-		delete(nicknameSerialMap, nickname)
+	if nickname, present := reverseMap(cfg.Names)[serial]; present {
+		delete(cfg.Names, nickname)
 	}
 
 	// Add the nickname serial mapping.
-	nicknameSerialMap[nickname] = serial
+	cfg.Names[nickname] = serial
 
-	return writeMapToFile(nicknameSerialMap, filename)
+	return writeConfig(cfg, filename)
 }
 
 var cmdMadbNameUnset = &cmdline.Command{
-	Runner: subCommandRunnerWithFilepath{runMadbNameUnset, getDefaultNameFilePath},
+	Runner: subCommandRunnerWithFilepath{runMadbNameUnset, getDefaultConfigFilePath},
 	Name:   "unset",
 	Short:  "Unset a nickname set by the 'madb name set' command.",
 	Long: `
@@ -122,15 +120,15 @@
 		return env.UsageErrorf("Not a valid device serial or name: %v", name)
 	}
 
-	nicknameSerialMap, err := readMapFromFile(filename)
+	cfg, err := readConfig(filename)
 	if err != nil {
 		return err
 	}
 
 	found := false
-	for nickname, serial := range nicknameSerialMap {
+	for nickname, serial := range cfg.Names {
 		if nickname == name || serial == name {
-			delete(nicknameSerialMap, nickname)
+			delete(cfg.Names, nickname)
 			found = true
 			break
 		}
@@ -140,11 +138,11 @@
 		return fmt.Errorf("The provided argument is neither a known nickname nor a device serial.")
 	}
 
-	return writeMapToFile(nicknameSerialMap, filename)
+	return writeConfig(cfg, filename)
 }
 
 var cmdMadbNameList = &cmdline.Command{
-	Runner: subCommandRunnerWithFilepath{runMadbNameList, getDefaultNameFilePath},
+	Runner: subCommandRunnerWithFilepath{runMadbNameList, getDefaultConfigFilePath},
 	Name:   "list",
 	Short:  "List all the existing nicknames.",
 	Long: `
@@ -153,7 +151,7 @@
 }
 
 func runMadbNameList(env *cmdline.Env, args []string, filename string) error {
-	nicknameSerialMap, err := readMapFromFile(filename)
+	cfg, err := readConfig(filename)
 	if err != nil {
 		return err
 	}
@@ -162,7 +160,7 @@
 	fmt.Println("Serial          Nickname")
 	fmt.Println("========================")
 
-	for nickname, serial := range nicknameSerialMap {
+	for nickname, serial := range cfg.Names {
 		fmt.Printf("%v\t%v\n", serial, nickname)
 	}
 
@@ -170,7 +168,7 @@
 }
 
 var cmdMadbNameClearAll = &cmdline.Command{
-	Runner: subCommandRunnerWithFilepath{runMadbNameClearAll, getDefaultNameFilePath},
+	Runner: subCommandRunnerWithFilepath{runMadbNameClearAll, getDefaultConfigFilePath},
 	Name:   "clear-all",
 	Short:  "Clear all the existing nicknames.",
 	Long: `
@@ -179,16 +177,13 @@
 }
 
 func runMadbNameClearAll(env *cmdline.Env, args []string, filename string) error {
-	return os.Remove(filename)
-}
-
-func getDefaultNameFilePath() (string, error) {
-	configDir, err := getConfigDir()
+	cfg, err := readConfig(filename)
 	if err != nil {
-		return "", err
+		return err
 	}
 
-	return filepath.Join(configDir, "nicknames"), nil
+	cfg.Names = make(map[string]string)
+	return writeConfig(cfg, filename)
 }
 
 func isValidDeviceSerial(serial string) bool {
diff --git a/name_test.go b/name_test.go
index 72306b1..a8fc1cf 100644
--- a/name_test.go
+++ b/name_test.go
@@ -14,7 +14,7 @@
 	filename := tempFilename(t)
 	defer os.Remove(filename)
 
-	var got, want map[string]string
+	var cfg *config
 	var err error
 
 	// Set a new nickname
@@ -22,11 +22,10 @@
 		t.Fatal(err)
 	}
 
-	if got, err = readMapFromFile(filename); err != nil {
+	if cfg, err = readConfig(filename); err != nil {
 		t.Fatal(err)
 	}
-	want = map[string]string{"NICKNAME1": "SERIAL1"}
-	if !reflect.DeepEqual(got, want) {
+	if got, want := cfg.Names, map[string]string{"NICKNAME1": "SERIAL1"}; !reflect.DeepEqual(got, want) {
 		t.Fatalf("unmatched results: got %v, want %v", got, want)
 	}
 
@@ -35,11 +34,10 @@
 		t.Fatal(err)
 	}
 
-	if got, err = readMapFromFile(filename); err != nil {
+	if cfg, err = readConfig(filename); err != nil {
 		t.Fatal(err)
 	}
-	want = map[string]string{"NICKNAME1": "SERIAL1", "NICKNAME2": "SERIAL2"}
-	if !reflect.DeepEqual(got, want) {
+	if got, want := cfg.Names, map[string]string{"NICKNAME1": "SERIAL1", "NICKNAME2": "SERIAL2"}; !reflect.DeepEqual(got, want) {
 		t.Fatalf("unmatched results: got %v, want %v", got, want)
 	}
 
@@ -48,11 +46,10 @@
 		t.Fatal(err)
 	}
 
-	if got, err = readMapFromFile(filename); err != nil {
+	if cfg, err = readConfig(filename); err != nil {
 		t.Fatal(err)
 	}
-	want = map[string]string{"NN1": "SERIAL1", "NICKNAME2": "SERIAL2"}
-	if !reflect.DeepEqual(got, want) {
+	if got, want := cfg.Names, map[string]string{"NN1": "SERIAL1", "NICKNAME2": "SERIAL2"}; !reflect.DeepEqual(got, want) {
 		t.Fatalf("unmatched results: got %v, want %v", got, want)
 	}
 
@@ -71,18 +68,17 @@
 	runMadbNameSet(nil, []string{"SERIAL2", "NICKNAME2"}, filename)
 	runMadbNameSet(nil, []string{"SERIAL3", "NICKNAME3"}, filename)
 
-	var got, want map[string]string
+	var cfg *config
 	var err error
 
 	// Unset by serial number.
 	if err = runMadbNameUnset(nil, []string{"SERIAL1"}, filename); err != nil {
 		t.Fatal(err)
 	}
-	if got, err = readMapFromFile(filename); err != nil {
+	if cfg, err = readConfig(filename); err != nil {
 		t.Fatal(err)
 	}
-	want = map[string]string{"NICKNAME2": "SERIAL2", "NICKNAME3": "SERIAL3"}
-	if !reflect.DeepEqual(got, want) {
+	if got, want := cfg.Names, map[string]string{"NICKNAME2": "SERIAL2", "NICKNAME3": "SERIAL3"}; !reflect.DeepEqual(got, want) {
 		t.Fatalf("unmatched results: got %v, want %v", got, want)
 	}
 
@@ -90,11 +86,10 @@
 	if err = runMadbNameUnset(nil, []string{"NICKNAME2"}, filename); err != nil {
 		t.Fatal(err)
 	}
-	if got, err = readMapFromFile(filename); err != nil {
+	if cfg, err = readConfig(filename); err != nil {
 		t.Fatal(err)
 	}
-	want = map[string]string{"NICKNAME3": "SERIAL3"}
-	if !reflect.DeepEqual(got, want) {
+	if got, want := cfg.Names, map[string]string{"NICKNAME3": "SERIAL3"}; !reflect.DeepEqual(got, want) {
 		t.Fatalf("unmatched results: got %v, want %v", got, want)
 	}
 
@@ -117,12 +112,27 @@
 	runMadbNameSet(nil, []string{"SERIAL2", "NICKNAME2"}, filename)
 	runMadbNameSet(nil, []string{"SERIAL3", "NICKNAME3"}, filename)
 
+	// Set up some default users. These users should be preserved after running
+	// the "name clear-all" command.
+	runMadbUserSet(nil, []string{"SERIAL1", "0"}, filename)
+	runMadbUserSet(nil, []string{"SERIAL2", "10"}, filename)
+
 	// Run the clear-all command. The file should be empty after running the command.
 	runMadbNameClearAll(nil, []string{}, filename)
 
-	// Check if the file is successfully deleted.
-	if _, err := os.Stat(filename); !os.IsNotExist(err) {
-		t.Fatalf("failed to delete file %q", filename)
+	cfg, err := readConfig(filename)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Make sure that the names are all deleted.
+	if got, want := cfg.Names, map[string]string{}; !reflect.DeepEqual(got, want) {
+		t.Fatalf("unmatched results: got %v, want %v", got, want)
+	}
+
+	// Make sure that the default user IDs are preserved.
+	if got, want := cfg.UserIDs, map[string]string{"SERIAL1": "0", "SERIAL2": "10"}; !reflect.DeepEqual(got, want) {
+		t.Fatalf("unmatched results: got %v, want %v", got, want)
 	}
 }
 
diff --git a/user.go b/user.go
index 651f5ea..75c87f4 100644
--- a/user.go
+++ b/user.go
@@ -6,8 +6,6 @@
 
 import (
 	"fmt"
-	"os"
-	"path/filepath"
 	"strconv"
 
 	"v.io/x/lib/cmdline"
@@ -46,7 +44,7 @@
 }
 
 var cmdMadbUserSet = &cmdline.Command{
-	Runner: subCommandRunnerWithFilepath{runMadbUserSet, getDefaultUserFilePath},
+	Runner: subCommandRunnerWithFilepath{runMadbUserSet, getDefaultConfigFilePath},
 	Name:   "set",
 	Short:  "Set a default user ID to be used for the given device.",
 	Long: `
@@ -104,19 +102,18 @@
 		return fmt.Errorf("Not a valid user ID: %v", userID)
 	}
 
-	// Get the <device_serial, user_id> mapping.
-	serialUserMap, err := readMapFromFile(filename)
+	cfg, err := readConfig(filename)
 	if err != nil {
 		return err
 	}
 
 	// Add the <device_serial, user_id> mapping for the specified device.
-	serialUserMap[serial] = userID
-	return writeMapToFile(serialUserMap, filename)
+	cfg.UserIDs[serial] = userID
+	return writeConfig(cfg, filename)
 }
 
 var cmdMadbUserUnset = &cmdline.Command{
-	Runner: subCommandRunnerWithFilepath{runMadbUserUnset, getDefaultUserFilePath},
+	Runner: subCommandRunnerWithFilepath{runMadbUserUnset, getDefaultConfigFilePath},
 	Name:   "unset",
 	Short:  "Unset the default user ID set by the 'madb user set' command.",
 	Long: `
@@ -144,19 +141,17 @@
 		return fmt.Errorf("Not a valid device serial: %v", serial)
 	}
 
-	// Get the <device_serial, user_id> mapping.
-	serialUserMap, err := readMapFromFile(filename)
+	cfg, err := readConfig(filename)
 	if err != nil {
 		return err
 	}
 
-	// Delete the <device_serial, user_id> mapping for the specified device.
-	delete(serialUserMap, serial)
-	return writeMapToFile(serialUserMap, filename)
+	delete(cfg.UserIDs, serial)
+	return writeConfig(cfg, filename)
 }
 
 var cmdMadbUserList = &cmdline.Command{
-	Runner: subCommandRunnerWithFilepath{runMadbUserList, getDefaultUserFilePath},
+	Runner: subCommandRunnerWithFilepath{runMadbUserList, getDefaultConfigFilePath},
 	Name:   "list",
 	Short:  "List all the existing default user IDs.",
 	Long: `
@@ -165,8 +160,7 @@
 }
 
 func runMadbUserList(env *cmdline.Env, args []string, filename string) error {
-	// Get the <device_serial, user_id> mapping.
-	serialUserMap, err := readMapFromFile(filename)
+	cfg, err := readConfig(filename)
 	if err != nil {
 		return err
 	}
@@ -175,7 +169,7 @@
 	fmt.Println("Device Serial    User ID")
 	fmt.Println("========================")
 
-	for s, u := range serialUserMap {
+	for s, u := range cfg.UserIDs {
 		fmt.Printf("%v\t%v\n", s, u)
 	}
 
@@ -183,7 +177,7 @@
 }
 
 var cmdMadbUserClearAll = &cmdline.Command{
-	Runner: subCommandRunnerWithFilepath{runMadbUserClearAll, getDefaultUserFilePath},
+	Runner: subCommandRunnerWithFilepath{runMadbUserClearAll, getDefaultConfigFilePath},
 	Name:   "clear-all",
 	Short:  "Clear all the existing default user settings.",
 	Long: `
@@ -194,14 +188,11 @@
 }
 
 func runMadbUserClearAll(env *cmdline.Env, args []string, filename string) error {
-	return os.Remove(filename)
-}
-
-func getDefaultUserFilePath() (string, error) {
-	configDir, err := getConfigDir()
+	cfg, err := readConfig(filename)
 	if err != nil {
-		return "", err
+		return err
 	}
 
-	return filepath.Join(configDir, "users"), nil
+	cfg.UserIDs = make(map[string]string)
+	return writeConfig(cfg, filename)
 }
diff --git a/user_test.go b/user_test.go
index c76d4a8..fae4bc4 100644
--- a/user_test.go
+++ b/user_test.go
@@ -14,7 +14,7 @@
 	filename := tempFilename(t)
 	defer os.Remove(filename)
 
-	var got, want map[string]string
+	var cfg *config
 	var err error
 
 	// Set a new nickname
@@ -22,11 +22,10 @@
 		t.Fatal(err)
 	}
 
-	if got, err = readMapFromFile(filename); err != nil {
+	if cfg, err = readConfig(filename); err != nil {
 		t.Fatal(err)
 	}
-	want = map[string]string{"SERIAL1": "0"}
-	if !reflect.DeepEqual(got, want) {
+	if got, want := cfg.UserIDs, map[string]string{"SERIAL1": "0"}; !reflect.DeepEqual(got, want) {
 		t.Fatalf("unmatched results: got %v, want %v", got, want)
 	}
 
@@ -35,11 +34,10 @@
 		t.Fatal(err)
 	}
 
-	if got, err = readMapFromFile(filename); err != nil {
+	if cfg, err = readConfig(filename); err != nil {
 		t.Fatal(err)
 	}
-	want = map[string]string{"SERIAL1": "0", "SERIAL2": "10"}
-	if !reflect.DeepEqual(got, want) {
+	if got, want := cfg.UserIDs, map[string]string{"SERIAL1": "0", "SERIAL2": "10"}; !reflect.DeepEqual(got, want) {
 		t.Fatalf("unmatched results: got %v, want %v", got, want)
 	}
 
@@ -48,11 +46,10 @@
 		t.Fatal(err)
 	}
 
-	if got, err = readMapFromFile(filename); err != nil {
+	if cfg, err = readConfig(filename); err != nil {
 		t.Fatal(err)
 	}
-	want = map[string]string{"SERIAL1": "20", "SERIAL2": "10"}
-	if !reflect.DeepEqual(got, want) {
+	if got, want := cfg.UserIDs, map[string]string{"SERIAL1": "20", "SERIAL2": "10"}; !reflect.DeepEqual(got, want) {
 		t.Fatalf("unmatched results: got %v, want %v", got, want)
 	}
 
@@ -74,18 +71,17 @@
 	runMadbUserSet(nil, []string{"SERIAL2", "0"}, filename)
 	runMadbUserSet(nil, []string{"SERIAL3", "10"}, filename)
 
-	var got, want map[string]string
+	var cfg *config
 	var err error
 
 	// Unset by serial number.
 	if err = runMadbUserUnset(nil, []string{"SERIAL1"}, filename); err != nil {
 		t.Fatal(err)
 	}
-	if got, err = readMapFromFile(filename); err != nil {
+	if cfg, err = readConfig(filename); err != nil {
 		t.Fatal(err)
 	}
-	want = map[string]string{"SERIAL2": "0", "SERIAL3": "10"}
-	if !reflect.DeepEqual(got, want) {
+	if got, want := cfg.UserIDs, map[string]string{"SERIAL2": "0", "SERIAL3": "10"}; !reflect.DeepEqual(got, want) {
 		t.Fatalf("unmatched results: got %v, want %v", got, want)
 	}
 }
@@ -99,11 +95,26 @@
 	runMadbUserSet(nil, []string{"SERIAL2", "0"}, filename)
 	runMadbUserSet(nil, []string{"SERIAL3", "10"}, filename)
 
-	// Run the clear-all command. The file should be empty after running the command.
+	// Set up some default nicknames. These nicknames should be preserved after
+	// running the "user clear-all" command.
+	runMadbNameSet(nil, []string{"SERIAL1", "NICKNAME1"}, filename)
+	runMadbNameSet(nil, []string{"SERIAL2", "NICKNAME2"}, filename)
+
+	// Run the clear-all command.
 	runMadbUserClearAll(nil, []string{}, filename)
 
-	// Check if the file is successfully deleted.
-	if _, err := os.Stat(filename); !os.IsNotExist(err) {
-		t.Fatalf("failed to delete file %q", filename)
+	cfg, err := readConfig(filename)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Make sure that the user IDs are all deleted.
+	if got, want := cfg.UserIDs, map[string]string{}; !reflect.DeepEqual(got, want) {
+		t.Fatalf("unmatched results: got %v, want %v", got, want)
+	}
+
+	// Make sure that the nicknames are preserved.
+	if got, want := cfg.Names, map[string]string{"NICKNAME1": "SERIAL1", "NICKNAME2": "SERIAL2"}; !reflect.DeepEqual(got, want) {
+		t.Fatalf("unmatched results: got %v, want %v", got, want)
 	}
 }