package main import ( "errors" "os" "path/filepath" "strings" "testing" "github.com/spf13/cobra" intdevice "netdome.biz/paul/qmk-rgb/internal/device" ) // The tool knows how to resolve --zone, but the shell only offers what the binary // tells it about, and it told it nothing: the flag was offered, its values were // not. These cases fix that, and they are worth fixing because --zone and the // profile names are the two places a user types something the tool already knows. func TestZoneCompletionOffersSubsystemsAndTheBoardsOwnNames(t *testing.T) { dir := t.TempDir() writeDefinition(t, dir, "impact80.json", `{ "name": "Impact 80", "vendorId": "0x36B0", "productId": "0x309F", "menus": [{"label":"Lighting","content":[ {"label":"logo","content":[{"label":"Effect","type":"dropdown", "content":["id_qmk_rgblight_effect",2,2],"options":["none"]}]}, {"label":"Backlight","content":[{"label":"Effect","type":"dropdown", "content":["id_qmk_rgb_matrix_effect",3,2],"options":["none"]}]}]}]}`) t.Cleanup(forceDefinitionsDir(t, dir)) got, directive := completeZoneNames(nil, nil, "") if directive != cobra.ShellCompDirectiveNoFileComp { t.Errorf("directive = %v, want NoFileComp so the shell does not fall back to paths", directive) } for _, want := range []string{"backlight", "rgblight", "rgb_matrix", "audio", "led_matrix", "logo", "Backlight"} { if !contains(got, want) { t.Errorf("completeZoneNames() = %v, want it to offer %q", got, want) } } // Nothing is offered twice: Backlight is both a label and close to // backlight, and a list with repeats reads as a bug. if len(got) != countDistinct(got) { t.Errorf("completeZoneNames() = %v, want no duplicates", got) } } // The shell is completing as the user types, so a prefix has to narrow the list. func TestZoneCompletionNarrowsToThePrefix(t *testing.T) { t.Cleanup(forceDefinitionsDir(t, t.TempDir())) got, directive := completeZoneNames(nil, nil, "rgb") if directive != cobra.ShellCompDirectiveNoFileComp { t.Errorf("directive = %v, want NoFileComp so the shell does not fall back to paths", directive) } for _, g := range got { if !strings.HasPrefix(g, "rgb") { t.Errorf("completeZoneNames(%q) = %v, want only names with that prefix", "rgb", got) } } if !contains(got, "rgb_matrix") { t.Errorf("completeZoneNames(\"rgb\") = %v, want rgb_matrix", got) } } // A definition that is not there must not break completion: a shell prints the // error text, which is worse than offering nothing. func TestZoneCompletionSurvivesAMissingDataDirectory(t *testing.T) { t.Cleanup(forceDefinitionsDir(t, filepath.Join(t.TempDir(), "absent"))) got, directive := completeZoneNames(nil, nil, "") if directive != cobra.ShellCompDirectiveNoFileComp { t.Errorf("directive = %v, want NoFileComp so the shell does not fall back to paths", directive) } if !contains(got, "rgb_matrix") { t.Errorf("completeZoneNames() = %v, want the QMK subsystem names at least", got) } } // --device takes the numbers keyboard info prints, which come from enumeration // alone and need no open keyboard. func TestDeviceCompletionOffersTheConnectedKeyboards(t *testing.T) { original := discoverAll discoverAll = func() ([]intdevice.Device, error) { return []intdevice.Device{{Index: 1}, {Index: 2}}, nil } t.Cleanup(func() { discoverAll = original }) got, directive := completeDeviceNumbers(nil, nil, "") if directive != cobra.ShellCompDirectiveNoFileComp { t.Errorf("directive = %v, want NoFileComp so the shell does not fall back to paths", directive) } if len(got) != 2 || got[0] != "1" || got[1] != "2" { t.Errorf("completeDeviceNumbers() = %v, want [1 2]", got) } } func TestDeviceCompletionSurvivesADiscoveryFailure(t *testing.T) { original := discoverAll discoverAll = func() ([]intdevice.Device, error) { return nil, errors.New("no keyboard") } t.Cleanup(func() { discoverAll = original }) got, directive := completeDeviceNumbers(nil, nil, "") if directive != cobra.ShellCompDirectiveNoFileComp { t.Errorf("directive = %v, want NoFileComp so the shell does not fall back to paths", directive) } if len(got) != 0 { t.Errorf("completeDeviceNumbers() = %v, want nothing when discovery fails", got) } } // load, save and delete take a profile name, and the names in profiles/ are // readable without a keyboard — which is the point, because the shell is asking // while nothing is plugged in. func TestProfileNameCompletionOffersTheFilesInTheDirectory(t *testing.T) { dir := t.TempDir() original := profilesDirOverride profilesDirOverride = dir t.Cleanup(func() { profilesDirOverride = original }) for _, name := range []string{"paul.json", "default.json", "notes.txt"} { if err := os.WriteFile(filepath.Join(dir, name), []byte("{}"), 0o600); err != nil { t.Fatal(err) } } for _, cmd := range []*cobra.Command{NewProfileLoadCmd(), NewProfileDeleteCmd()} { got, _ := completeProfileNames(cmd, nil, "") if len(got) != 2 || got[0] != "default" || got[1] != "paul" { t.Errorf("completeProfileNames() = %v, want [default paul]", got) } } } func TestProfileNameCompletionSurvivesAMissingDirectory(t *testing.T) { original := profilesDirOverride profilesDirOverride = filepath.Join(t.TempDir(), "absent") t.Cleanup(func() { profilesDirOverride = original }) got, directive := completeProfileNames(nil, nil, "") if directive != cobra.ShellCompDirectiveNoFileComp { t.Errorf("directive = %v, want NoFileComp so the shell does not fall back to paths", directive) } if len(got) != 0 { t.Errorf("completeProfileNames() = %v, want nothing", got) } } func contains(list []string, want string) bool { for _, s := range list { if s == want { return true } } return false } func countDistinct(list []string) int { seen := make(map[string]bool, len(list)) for _, s := range list { seen[s] = true } return len(seen) }