| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308 |
- package main
- import (
- "os"
- "path/filepath"
- "strings"
- "testing"
- intrgb "netdome.biz/paul/qmk-rgb/internal/rgb"
- intvia "netdome.biz/paul/qmk-rgb/internal/via"
- )
- // generateStub is a keyboard that answers an effect range, so a generated names
- // file can be checked without hardware. The Vial fields are what a keyboard
- // running Vial firmware would answer; a zero version reads as one that is not
- // Vial.
- type generateStub struct {
- tops map[intvia.Channel]int
- vial bool
- vialIDs []uint16
- }
- func (g generateStub) EffectTop(ch intvia.Channel) (int, error) { return g.tops[ch], nil }
- func (g generateStub) VialVersion() (uint32, bool, error) {
- if !g.vial {
- return 0, false, nil
- }
- return 6, true, nil
- }
- func (g generateStub) VialEffectIDs() ([]uint16, error) { return g.vialIDs, nil }
- func (g generateStub) GetValue(intvia.Channel, uint8) ([]byte, error) { return []byte{0}, nil }
- func (g generateStub) SetValue(intvia.Channel, uint8, uint8) error { return nil }
- func (g generateStub) SetColor(intvia.Channel, uint8, uint8) error { return nil }
- func (g generateStub) DetectChannels() ([]intvia.Channel, error) { return nil, nil }
- func (g generateStub) Close() error { return nil }
- // stubGenerateTarget makes the connected keyboard a fixed one. The identifiers are
- // a parameter because a board the binary carries a definition for is a different
- // case from one it does not, and a stub for the wrong board would test nothing.
- func stubGenerateTarget(t *testing.T, vendorID, productID uint16, channels []intvia.Channel, stub generateStub) {
- t.Helper()
- original := openTarget
- t.Cleanup(func() { openTarget = original })
- openTarget = func(string) (rgbProtocol, targetDeviceData, []intvia.Channel, error) {
- return stub, stubTargetData(vendorID, productID), channels, nil
- }
- }
- func forceGenerateRestore(t *testing.T) func() {
- t.Helper()
- original := generateForce
- return func() { generateForce = original }
- }
- // forceNamesDir points the names directory at a test directory. Without it a test
- // that writes a names file writes into the user's real data directory, which is
- // what happened when this override did not exist yet.
- func forceNamesDir(t *testing.T, dir string) func() {
- t.Helper()
- original := namesDirOverride
- namesDirOverride = dir
- return func() { namesDirOverride = original }
- }
- // generateSetup points every writable directory at one temporary directory, so a
- // test cannot reach the user's.
- // generateSetup points the writable directories at temporary ones and returns the
- // names directory. They are two directories on purpose: a names file in the
- // definitions directory parses as a definition — it carries the board's
- // identifiers and no menus — and takes the board's place, which is exactly what
- // TestNamesFileInTheDefinitionsDirectoryIsNotADefinition is about.
- func generateSetup(t *testing.T) string {
- t.Helper()
- names := t.TempDir()
- t.Cleanup(definitionFlagRestore(t))
- t.Cleanup(forceDefinitionsDir(t, t.TempDir()))
- t.Cleanup(forceNamesDir(t, names))
- t.Cleanup(forceGenerateRestore(t))
- return names
- }
- func runGenerate(t *testing.T) (string, string) {
- t.Helper()
- cmd := NewKeyboardDefinitionsGenerateCmd()
- var out, errOut strings.Builder
- cmd.SetOut(&out)
- cmd.SetErr(&errOut)
- if err := cmd.Execute(); err != nil {
- t.Fatalf("definitions generate error = %v (stderr %q)", err, errOut.String())
- }
- return out.String(), errOut.String()
- }
- func onlyNamesFile(t *testing.T, dir string) string {
- t.Helper()
- matches, err := filepath.Glob(filepath.Join(dir, "*.json"))
- if err != nil {
- t.Fatal(err)
- }
- if len(matches) != 1 {
- t.Fatalf("names dir = %v, want one file", matches)
- }
- return matches[0]
- }
- func mustRead(t *testing.T, path string) string {
- t.Helper()
- data, err := os.ReadFile(path)
- if err != nil {
- t.Fatal(err)
- }
- return string(data)
- }
- // A generated file names no effect, so a board reports the same "no names" it
- // reported before one existed. A name the tool wrote would be indistinguishable
- // from the manufacturer's, and nothing in a file can be read back off a keyboard
- // to check it.
- func TestGeneratedNamesFileNamesNoEffectUntilTheUserFillsItIn(t *testing.T) {
- dir := generateSetup(t)
- stubGenerateTarget(t, 0x1234, 0x5678, []intvia.Channel{intvia.ChannelRgbMatrix},
- generateStub{tops: map[intvia.Channel]int{intvia.ChannelRgbMatrix: 45}})
- runGenerate(t)
- path := onlyNamesFile(t, dir)
- names, err := intrgbLoadNamesFile(path)
- if err != nil {
- t.Fatalf("LoadNamesFile(%s) error = %v; the generated file has to load", path, err)
- }
- if got := len(names.Effects(intvia.ChannelRgbMatrix)); got != 0 {
- t.Errorf("effects on rgb_matrix = %d, want 0 until the names are written in", got)
- }
- // The slots are still there, which is the other half: the file says which IDs
- // the board has and leaves the naming to the user.
- body := mustRead(t, path)
- for _, want := range []string{`"0": ""`, `"7": ""`, `"45": ""`} {
- if !strings.Contains(body, want) {
- t.Errorf("generated file has no empty slot %s", want)
- }
- }
- // And it is one line per effect, not a nested menu description to dig through.
- if lines := strings.Count(body, "\n"); lines > 60 {
- t.Errorf("generated file is %d lines, want roughly one per effect", lines)
- }
- }
- // A names file is the one the user would have run the command to write, so a
- // second run must not replace it.
- func TestGenerateDoesNotReplaceAStoredNamesFile(t *testing.T) {
- dir := generateSetup(t)
- stubGenerateTarget(t, 0x1234, 0x5678, []intvia.Channel{intvia.ChannelRgbMatrix},
- generateStub{tops: map[intvia.Channel]int{intvia.ChannelRgbMatrix: 45}})
- written := `{"vendorId":"0x1234","productId":"0x5678","channels":{"rgb_matrix":{"7":"mine"}}}`
- if err := os.WriteFile(filepath.Join(dir, "board.json"), []byte(written), 0o644); err != nil {
- t.Fatal(err)
- }
- cmd := NewKeyboardDefinitionsGenerateCmd()
- var out, errOut strings.Builder
- cmd.SetOut(&out)
- cmd.SetErr(&errOut)
- err := cmd.Execute()
- if err == nil {
- t.Fatal("definitions generate = nil error, want a refusal to replace a stored names file")
- }
- if !strings.Contains(err.Error(), "--force") {
- t.Errorf("error = %q, want it to offer --force", err)
- }
- if got := mustRead(t, filepath.Join(dir, "board.json")); got != written {
- t.Errorf("stored file = %q, want it untouched (%q)", got, written)
- }
- }
- // The note beside the file is what carries the names, because a JSON file cannot:
- // the tool would have to read comments and VIA's parser would reject them. It has
- // to say who wrote what, or a name is a guess wearing a count.
- func TestGeneratedNoteCarriesTheSpellingsAndWhoWroteThem(t *testing.T) {
- dir := generateSetup(t)
- stubGenerateTarget(t, 0x1234, 0x5678, []intvia.Channel{intvia.ChannelRgbMatrix},
- generateStub{tops: map[intvia.Channel]int{intvia.ChannelRgbMatrix: 45}})
- runGenerate(t)
- note := mustRead(t, strings.TrimSuffix(onlyNamesFile(t, dir), ".json")+spottedNoteSuffix)
- for _, want := range []string{
- "rgb_matrix (channel 3)",
- "rainbow_moving_chevron",
- "boards:",
- "keychron",
- string(slotsMeasured),
- } {
- if !strings.Contains(note, want) {
- t.Errorf("note does not carry %q", want)
- }
- }
- }
- // A Vial keyboard lists its own rgb_matrix effect IDs, and the tool must use those
- // rather than writing above the top: the list is the firmware answering without
- // being touched.
- func TestGenerateUsesVialsOwnListWhenTheFirmwareIsVial(t *testing.T) {
- dir := generateSetup(t)
- stubGenerateTarget(t, 0x1234, 0x5678,
- []intvia.Channel{intvia.ChannelRgbMatrix, intvia.ChannelRgblight},
- generateStub{
- tops: map[intvia.Channel]int{intvia.ChannelRgbMatrix: 45, intvia.ChannelRgblight: 6},
- vial: true,
- vialIDs: []uint16{0, 1, 2, 40, 45},
- })
- out, _ := runGenerate(t)
- if !strings.Contains(out, "Vial firmware") {
- t.Errorf("stdout = %q, want it to say the keyboard is Vial", out)
- }
- if !strings.Contains(out, experimentalVial) {
- t.Errorf("stdout = %q, want the Vial path marked %s", out, experimentalVial)
- }
- // rgb_matrix keeps the firmware's own numbering, gaps and all; rgblight has no
- // Vial list and falls back to the clamp.
- if !strings.Contains(out, "0, 1, 2, 40, 45") {
- t.Errorf("stdout = %q, want the rgb_matrix slots in the firmware's numbering", out)
- }
- if !strings.Contains(out, string(slotsReported)) || !strings.Contains(out, string(slotsMeasured)) {
- t.Errorf("stdout = %q, want both sources named per channel", out)
- }
- // The note must not put QMK's spellings next to numbers that are not QMK's.
- note := mustRead(t, strings.TrimSuffix(onlyNamesFile(t, dir), ".json")+spottedNoteSuffix)
- if strings.Contains(note, "rainbow_moving_chevron") {
- t.Errorf("note = %q, want no QMK spellings beside a Vial numbering", note)
- }
- if !strings.Contains(note, "VIALRGB_EFFECT_*") {
- t.Errorf("note = %q, want it to say the numbering is the firmware's own", note)
- }
- }
- // A Vial keyboard built without VIALRGB_ENABLE answers nothing, and a board that
- // cannot list its effects can still be asked by writing above the top.
- func TestGenerateFallsBackToTheClampWhenVialListsNothing(t *testing.T) {
- dir := generateSetup(t)
- stubGenerateTarget(t, 0x1234, 0x5678, []intvia.Channel{intvia.ChannelRgbMatrix},
- generateStub{tops: map[intvia.Channel]int{intvia.ChannelRgbMatrix: 45}, vial: true})
- out, _ := runGenerate(t)
- if !strings.Contains(out, string(slotsMeasured)) {
- t.Errorf("stdout = %q, want the clamp used when Vial lists nothing", out)
- }
- note := mustRead(t, strings.TrimSuffix(onlyNamesFile(t, dir), ".json")+spottedNoteSuffix)
- if !strings.Contains(note, "rainbow_moving_chevron") {
- t.Errorf("note = %q, want the QMK spellings again, which is what the clamp gives", note)
- }
- }
- // The point of storing names apart from the definition: a name the user writes
- // replaces the vendor's for that one effect, and the rest of the file still stands.
- // The board is the Impact 80, whose definition the binary carries, so "the rest"
- // means real names the vendor wrote.
- func TestANameTheUserWroteOverridesOneEffectAndLeavesTheRest(t *testing.T) {
- generateSetup(t)
- names := t.TempDir()
- t.Cleanup(forceNamesDir(t, names))
- body := `{"name":"Impact 80","vendorId":"0x36B0","productId":"0x309F",
- "channels":{"rgb_matrix":{"7":"my_chevron"}}}`
- if err := os.WriteFile(filepath.Join(names, "impact80.json"), []byte(body), 0o644); err != nil {
- t.Fatal(err)
- }
- catalog, _, err := resolveCatalogFor(0x36B0, 0x309F)
- if err != nil {
- t.Fatalf("resolveCatalogFor() error = %v", err)
- }
- if got := catalog.EffectName(intvia.ChannelRgbMatrix, 7); got != "my_chevron" {
- t.Errorf("EffectName(7) = %q, want the user's name", got)
- }
- if got := catalog.EffectSource(intvia.ChannelRgbMatrix, 7); got != intrgbSourceUser {
- t.Errorf("EffectSource(7) = %q, want %q", got, intrgbSourceUser)
- }
- // ID 8 is band_pinwheel_sat in the Impact 80's own file, and the user's file
- // said nothing about it, so it has to still be the vendor's name and carry no
- // source of its own.
- if got := catalog.EffectName(intvia.ChannelRgbMatrix, 8); got != "band_pinwheel_sat" {
- t.Errorf("EffectName(8) = %q, want the vendor's name left alone", got)
- }
- if got := catalog.EffectSource(intvia.ChannelRgbMatrix, 8); got != intrgbSourceVendor {
- t.Errorf("EffectSource(8) = %q, want an empty source for the vendor's", got)
- }
- }
- // intrgbLoadNamesFile and intrgbSourceUser keep the test reading through the
- // internal package without importing it twice under two names.
- func intrgbLoadNamesFile(path string) (*intrgb.Names, error) { return intrgb.LoadNamesFile(path) }
- const (
- intrgbSourceUser = intrgb.SourceUser
- intrgbSourceVendor = intrgb.SourceVendor
- )
|