package main import ( "os" "path/filepath" "strings" "testing" ) // Profiles and definitions are the tool's own files, and a binary installed with // `go install` sits in $GOPATH/bin where neither of them exists. They need a // per-user directory, and they need the same one on Linux, macOS and Windows, // which is what os.UserConfigDir answers and a hardcoded ~/.config does not. func TestDataDirectoriesAreTheUserDirectory(t *testing.T) { userDir := t.TempDir() t.Cleanup(stubUserConfigDir(t, userDir)) t.Cleanup(func() { profilesDirOverride = "" }) t.Cleanup(func() { definitionsDirOverride = "" }) base := filepath.Join(userDir, "qmk-rgb-tool") for _, tc := range []struct { name string got string want string }{ {"definitions", definitionsPath(), filepath.Join(base, "definitions")}, {"profiles", profilesPath(), filepath.Join(base, "profiles")}, } { if tc.got != tc.want { t.Errorf("%sPath() = %q, want %q", tc.name, tc.got, tc.want) } } } // The directories used to be searched: next to the executable, then the working // directory, then the user's. Both reasons for that are gone, and a test has to // say so, because the search is the kind of thing that comes back. // // A search path for a definition hides the file `keyboard fetch` just wrote, // because a definitions directory beside the binary exists for as long as the // binary is run from a checkout, and it wins. A search path for a profile finds a // same-named file in a checkout and loads that one instead of the one the user // saved, so which file `load ` reads depends on where the command is run. func TestDataDirectoriesAreNotSearched(t *testing.T) { userDir := t.TempDir() t.Cleanup(stubUserConfigDir(t, userDir)) t.Cleanup(func() { profilesDirOverride = "" }) t.Cleanup(func() { definitionsDirOverride = "" }) base := filepath.Join(userDir, "qmk-rgb-tool") // A repository checkout with both directories beside the binary, and the same // two in the working directory, so every step of the old order would match. here := t.TempDir() for _, dir := range []string{here, filepath.Join(here, "sub")} { for _, name := range []string{"definitions", "profiles"} { if err := os.MkdirAll(filepath.Join(dir, name), 0o755); err != nil { t.Fatal(err) } } } t.Chdir(here) if got, want := definitionsPath(), filepath.Join(base, "definitions"); got != want { t.Errorf("definitionsPath() = %q, want %q", got, want) } if got, want := profilesPath(), filepath.Join(base, "profiles"); got != want { t.Errorf("profilesPath() = %q, want %q", got, want) } } // A profile written is a profile that reads back, which a search path broke: the // save went to the user's directory and the read stopped at a checkout's. func TestProfileIsReadBackFromWhereItIsWritten(t *testing.T) { userDir := t.TempDir() t.Cleanup(stubUserConfigDir(t, userDir)) t.Cleanup(func() { profilesDirOverride = "" }) // A checkout's profiles directory, holding a same-named file with different // content: if this one is loaded, the read is still searching. here := t.TempDir() if err := os.MkdirAll(filepath.Join(here, "profiles"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(here, "profiles", "lava.json"), []byte(`{"name":"lava","version":1,"zones":{"Backlight":{"effect":"wave"}}}`), 0o644); err != nil { t.Fatal(err) } t.Chdir(here) if err := (&Profile{Name: "lava", Version: 1}).Save(); err != nil { t.Fatalf("Save(): %v", err) } if _, err := os.Stat(filepath.Join(here, "profiles", "lava.json")); err != nil { t.Fatalf("Save() did not write into the working directory's profiles: %v", err) } loaded, err := LoadProfile("lava") if err != nil { t.Fatalf("LoadProfile(lava) after Save(): %v", err) } if zone, ok := loaded.Zones["Backlight"]; ok && zone.Effect != "" { t.Errorf("loaded the checkout's profile: effect = %q, want the saved one, which has no zones", zone.Effect) } } // A profile that is not there says where it looked, because "not found" with no // path is the one error a user cannot act on. func TestMissingProfileNamesTheDirectory(t *testing.T) { userDir := t.TempDir() t.Cleanup(stubUserConfigDir(t, userDir)) t.Cleanup(func() { profilesDirOverride = "" }) _, err := LoadProfile("nope") if err == nil { t.Fatal("LoadProfile(nope) = nil error, want one") } if !strings.Contains(err.Error(), filepath.Join("qmk-rgb-tool", "profiles")) { t.Errorf("LoadProfile(nope) = %q, want it to name the directory", err) } } // A list of profiles has no duplicates to fold, because there is one directory // they come from. The names are still the ones on disk, without the extension. func TestProfilesAreListedFromTheUserDirectory(t *testing.T) { userDir := t.TempDir() t.Cleanup(stubUserConfigDir(t, userDir)) t.Cleanup(func() { profilesDirOverride = "" }) dir := filepath.Join(userDir, "qmk-rgb-tool", "profiles") if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatal(err) } for _, n := range []string{"lava", "wave"} { if err := os.WriteFile(filepath.Join(dir, n+".json"), []byte("{}"), 0o644); err != nil { t.Fatal(err) } } if err := os.WriteFile(filepath.Join(dir, "notes.txt"), []byte(""), 0o644); err != nil { t.Fatal(err) } got, err := ListProfiles() if err != nil { t.Fatalf("ListProfiles(): %v", err) } if len(got) != 2 { t.Fatalf("ListProfiles() = %v, want lava and wave", got) } found := map[string]bool{} for _, n := range got { found[n] = true } if !found["lava"] || !found["wave"] { t.Errorf("ListProfiles() = %v, want lava and wave", got) } } // A directory that is not there is not an error: a user who has never saved a // profile has none, and `list` says so by printing nothing. func TestListingWithoutAProfilesDirectory(t *testing.T) { userDir := t.TempDir() t.Cleanup(stubUserConfigDir(t, userDir)) t.Cleanup(func() { profilesDirOverride = "" }) got, err := ListProfiles() if err != nil { t.Fatalf("ListProfiles(): %v, want no error for a directory that does not exist", err) } if len(got) != 0 { t.Errorf("ListProfiles() = %v, want none", got) } } // The user directory is reported so the commands can say where a file came from. func TestDataDirectoryIsReportedForTheUser(t *testing.T) { userDir := t.TempDir() t.Cleanup(stubUserConfigDir(t, userDir)) t.Cleanup(func() { definitionsDirOverride = "" }) got := definitionsPath() if got == "" { t.Fatal("definitionsPath() = \"\", want a path") } if !filepath.IsAbs(got) { t.Errorf("definitionsPath() = %q, want an absolute path so it can be printed", got) } if !strings.Contains(describeDataDir(got), "your user directory") { t.Errorf("describeDataDir(%q) = %q, want it marked as the user's", got, describeDataDir(got)) } } // stubUserConfigDir points the platform's configuration directory at a temp one. func stubUserConfigDir(t *testing.T, dir string) func() { t.Helper() original := userConfigDir userConfigDir = func() (string, error) { return dir, nil } return func() { userConfigDir = original } }