feat(config): migrate old config files to the new format

In commit 178b658, the old config files, "nicknames" and "users", were
merged into a single "config" file. From this CL, when madb sees the
old config files, it migrates those files into the new "config" file
so that the user doesn't lose all the nicknames and user IDs when
upgrading madb to a new version.

Change-Id: I6f376986dd55211729d0a5eb70dc885d26a93d9d
diff --git a/madb.go b/madb.go
index 8020832..b6560a1 100644
--- a/madb.go
+++ b/madb.go
@@ -362,6 +362,14 @@
 	UserIDs map[string]string
 }
 
+func newConfig() *config {
+	return &config{
+		Names:   make(map[string]string),
+		Groups:  make(map[string][]string),
+		UserIDs: make(map[string]string),
+	}
+}
+
 // Returns the config dir located at "~/.madb"
 func getConfigDir() (string, error) {
 	home := os.Getenv("HOME")
@@ -374,9 +382,68 @@
 		return "", err
 	}
 
+	if err := migrateOldConfigFiles(configDir); err != nil {
+		fmt.Fprintf(os.Stderr, "WARNING: Could not successfully migrate the old config files to the newer format: %v", err)
+	}
+
 	return configDir, nil
 }
 
+// migrateOldConfigFiles checks if there are old config files (for madb v1.x) in
+// the provided config directory. If there are, it migrates these configs to the
+// new format, so that users can preserve their device nicknames and user IDs
+// when upgrading madb to a newer version.
+// TODO(youngseokyoon): remove this migration code in the future.
+func migrateOldConfigFiles(configDir string) error {
+	// Do not try migrating if the new format "config" file already exists.
+	configFile := filepath.Join(configDir, "config")
+	if _, err := os.Stat(configFile); err == nil {
+		return nil
+	}
+
+	cfg := newConfig()
+	if err := migrateOldConfig(configDir, "nicknames", &cfg.Names); err != nil {
+		return err
+	}
+	if err := migrateOldConfig(configDir, "users", &cfg.UserIDs); err != nil {
+		return err
+	}
+	return writeConfig(cfg, configFile)
+}
+
+// migrateOldConfig reads an old config file, which contains a JSON-encoded map,
+// and writes the contents to the given map pointer (data).
+func migrateOldConfig(configDir, filename string, data *map[string]string) error {
+	configFile := filepath.Join(configDir, filename)
+	if _, err := os.Stat(configFile); os.IsNotExist(err) {
+		return nil
+	}
+
+	f, err := os.Open(configFile)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	decoder := json.NewDecoder(f)
+
+	if err := decoder.Decode(data); err != nil {
+		data = new(map[string]string)
+		return fmt.Errorf("Could not read the old config file %q: %v", filename, err)
+	}
+
+	fmt.Printf("NOTE: Migrating the %q file to the newer format.\n", filename)
+
+	// Rename the old config as a backup
+	if err := os.Rename(configFile, configFile+".bak"); err != nil {
+		return fmt.Errorf("Could not rename the %q file: %v", filename, err)
+	}
+
+	fmt.Printf("NOTE: The backup file can be found at %q.\n", filepath.Join(configDir, filename+".bak"))
+
+	return nil
+}
+
 // getDefaultConfigFilePath returns the default location of the config file.
 func getDefaultConfigFilePath() (string, error) {
 	configDir, err := getConfigDir()
@@ -391,13 +458,10 @@
 // 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)
+	result := newConfig()
 
 	// 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.Groups = make(map[string][]string)
-		result.UserIDs = make(map[string]string)
 		return result, nil
 	}
 
diff --git a/madb_test.go b/madb_test.go
index e9bf5f0..2444704 100644
--- a/madb_test.go
+++ b/madb_test.go
@@ -7,6 +7,7 @@
 import (
 	"bytes"
 	"fmt"
+	"io"
 	"io/ioutil"
 	"os"
 	"path/filepath"
@@ -584,3 +585,120 @@
 		}
 	}
 }
+
+func TestConfigMigration(t *testing.T) {
+	tests := []struct {
+		configDir string
+		fileMap   map[string]string
+		want      config
+	}{
+		{
+			"testdata/configs/newFormat",
+			map[string]string{"config": "config"},
+			config{
+				Version: version,
+				Names:   map[string]string{"nickname01": "serial01", "nickname02": "serial02"},
+				Groups:  map[string][]string{},
+				UserIDs: map[string]string{"serial01": "10"},
+			},
+		},
+		{
+			"testdata/configs/oldFormatBoth",
+			map[string]string{"": "config", "nicknames": "nicknames.bak", "users": "users.bak"},
+			config{
+				Version: version,
+				Names:   map[string]string{"nickname01": "serial01", "nickname02": "serial02"},
+				Groups:  map[string][]string{},
+				UserIDs: map[string]string{"serial01": "10"},
+			},
+		},
+		{
+			"testdata/configs/oldFormatNicknamesOnly",
+			map[string]string{"": "config", "nicknames": "nicknames.bak"},
+			config{
+				Version: version,
+				Names:   map[string]string{"nickname01": "serial01", "nickname02": "serial02"},
+				Groups:  map[string][]string{},
+				UserIDs: map[string]string{},
+			},
+		},
+		{
+			"testdata/configs/oldFormatUsersOnly",
+			map[string]string{"": "config", "users": "users.bak"},
+			config{
+				Version: version,
+				Names:   map[string]string{},
+				Groups:  map[string][]string{},
+				UserIDs: map[string]string{"serial01": "10"},
+			},
+		},
+	}
+
+	for i, test := range tests {
+		// Copy the files in configDir to a temporary directory.
+		tempConfigDir, err := ioutil.TempDir("", "madbConfigTest")
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer os.RemoveAll(tempConfigDir)
+
+		files, err := ioutil.ReadDir(test.configDir)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		for _, file := range files {
+			src, err := os.Open(filepath.Join(test.configDir, file.Name()))
+			if err != nil {
+				t.Fatal(err)
+			}
+			dst, err := os.Create(filepath.Join(tempConfigDir, file.Name()))
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			_, err = io.Copy(dst, src)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			src.Close()
+			dst.Close()
+		}
+
+		// Run the migration.
+		migrateOldConfigFiles(tempConfigDir)
+
+		// Check the resulting config
+		cfg, err := readConfig(filepath.Join(tempConfigDir, "config"))
+		if err != nil {
+			t.Fatal(err)
+		}
+		if got, want := *cfg, test.want; !reflect.DeepEqual(got, want) {
+			t.Fatalf("unmatched results for tests[%v]: got %v, want %v", i, got, want)
+		}
+
+		// Check the file mapping.
+		for oldFile, newFile := range test.fileMap {
+			if oldFile == "" {
+				if _, err := os.Stat(filepath.Join(tempConfigDir, newFile)); os.IsNotExist(err) {
+					t.Fatalf("missing an expected config file %q", newFile)
+				}
+				continue
+			}
+
+			oldBytes, err := ioutil.ReadFile(filepath.Join(test.configDir, oldFile))
+			if err != nil {
+				t.Fatal(err)
+			}
+			newBytes, err := ioutil.ReadFile(filepath.Join(tempConfigDir, newFile))
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			if bytes.Compare(oldBytes, newBytes) != 0 {
+				t.Fatalf("unmatched file contents between %q and %q.", oldFile, newFile)
+			}
+		}
+	}
+}
diff --git a/testdata/configs/newFormat/config b/testdata/configs/newFormat/config
new file mode 100644
index 0000000..0776102
--- /dev/null
+++ b/testdata/configs/newFormat/config
@@ -0,0 +1 @@
+{"Version":"v1.1.1-develop","Names":{"nickname01":"serial01","nickname02":"serial02"},"Groups":{},"UserIDs":{"serial01":"10"}}
diff --git a/testdata/configs/oldFormatBoth/nicknames b/testdata/configs/oldFormatBoth/nicknames
new file mode 100644
index 0000000..99b1adb
--- /dev/null
+++ b/testdata/configs/oldFormatBoth/nicknames
@@ -0,0 +1 @@
+{"nickname01":"serial01","nickname02":"serial02"}
diff --git a/testdata/configs/oldFormatBoth/users b/testdata/configs/oldFormatBoth/users
new file mode 100644
index 0000000..bbf7890
--- /dev/null
+++ b/testdata/configs/oldFormatBoth/users
@@ -0,0 +1 @@
+{"serial01":"10"}
diff --git a/testdata/configs/oldFormatNicknamesOnly/nicknames b/testdata/configs/oldFormatNicknamesOnly/nicknames
new file mode 100644
index 0000000..99b1adb
--- /dev/null
+++ b/testdata/configs/oldFormatNicknamesOnly/nicknames
@@ -0,0 +1 @@
+{"nickname01":"serial01","nickname02":"serial02"}
diff --git a/testdata/configs/oldFormatUsersOnly/users b/testdata/configs/oldFormatUsersOnly/users
new file mode 100644
index 0000000..bbf7890
--- /dev/null
+++ b/testdata/configs/oldFormatUsersOnly/users
@@ -0,0 +1 @@
+{"serial01":"10"}