definition_test.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. package main
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "net/http"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "testing"
  10. intdevice "netdome.biz/paul/qmk-rgb/internal/device"
  11. intvia "netdome.biz/paul/qmk-rgb/internal/via"
  12. )
  13. func TestFetchDefinitionStoresTheFileForTheBoard(t *testing.T) {
  14. dir := t.TempDir()
  15. t.Cleanup(definitionFlagRestore(t))
  16. t.Cleanup(forceDefinitionsDir(t, dir))
  17. stubPrepareTarget(t, 0x1234, 0x5678)
  18. served := `{"name":"Test Board","vendorProductId":305419896,"menus":[]}`
  19. var requested []string
  20. stubHTTPGet(t, func(url string) ([]byte, int, error) {
  21. requested = append(requested, url)
  22. if strings.Contains(url, "/v3/") {
  23. return []byte(served), http.StatusOK, nil
  24. }
  25. return nil, http.StatusNotFound, errors.New("404")
  26. })
  27. cmd := NewKeyboardFetchCmd()
  28. var out, errOut strings.Builder
  29. cmd.SetOut(&out)
  30. cmd.SetErr(&errOut)
  31. if err := cmd.Execute(); err != nil {
  32. t.Fatalf("keyboard fetch error = %v (stderr %q)", err, errOut.String())
  33. }
  34. if len(requested) != 1 || !strings.Contains(requested[0], "/v3/305419896.json") {
  35. t.Errorf("requested = %v, want one v3 URL for vendorProductId 305419896", requested)
  36. }
  37. if !strings.Contains(out.String(), "Test Board") {
  38. t.Errorf("stdout = %q, want the board name", out.String())
  39. }
  40. entries, err := os.ReadDir(dir)
  41. if err != nil {
  42. t.Fatal(err)
  43. }
  44. if len(entries) != 1 || !strings.HasSuffix(entries[0].Name(), ".json") {
  45. t.Fatalf("definitions dir = %v, want one JSON file", entries)
  46. }
  47. }
  48. // A definition in the user directory may be one the user has edited, and nothing
  49. // on disk says which it is. So a second fetch must not replace it, and must say
  50. // that the file is there rather than write over it silently.
  51. func TestFetchDoesNotReplaceAStoredDefinition(t *testing.T) {
  52. dir := t.TempDir()
  53. t.Cleanup(definitionFlagRestore(t))
  54. t.Cleanup(forceDefinitionsDir(t, dir))
  55. t.Cleanup(forceFetchRestore(t))
  56. stubPrepareTarget(t, 0x1234, 0x5678)
  57. edited := `{"name":"Test Board","vendorProductId":305419896,"menus":[],"note":"hand edited"}`
  58. if err := os.WriteFile(filepath.Join(dir, "test_board.json"), []byte(edited), 0o644); err != nil {
  59. t.Fatal(err)
  60. }
  61. // No HTTP stub: a request would panic here, which is the point. The user is
  62. // told the file exists without waiting on the network.
  63. cmd := NewKeyboardFetchCmd()
  64. var out, errOut strings.Builder
  65. cmd.SetOut(&out)
  66. cmd.SetErr(&errOut)
  67. err := cmd.Execute()
  68. if err == nil {
  69. t.Fatal("keyboard fetch = nil error, want a refusal to replace a stored definition")
  70. }
  71. if !strings.Contains(err.Error(), "already at") || !strings.Contains(err.Error(), "--force") {
  72. t.Errorf("error = %q, want it to name the file and --force", err)
  73. }
  74. got, readErr := os.ReadFile(filepath.Join(dir, "test_board.json"))
  75. if readErr != nil {
  76. t.Fatal(readErr)
  77. }
  78. if string(got) != edited {
  79. t.Errorf("stored file = %q, want it untouched (%q)", got, edited)
  80. }
  81. }
  82. // --force is the only way to replace a stored definition, and it has to say that
  83. // is what happened, because the file it wrote over is gone.
  84. func TestFetchForceReplacesAStoredDefinition(t *testing.T) {
  85. dir := t.TempDir()
  86. t.Cleanup(definitionFlagRestore(t))
  87. t.Cleanup(forceDefinitionsDir(t, dir))
  88. t.Cleanup(forceFetchRestore(t))
  89. stubPrepareTarget(t, 0x1234, 0x5678)
  90. if err := os.WriteFile(filepath.Join(dir, "test_board.json"),
  91. []byte(`{"name":"Test Board","vendorProductId":305419896,"menus":[],"note":"hand edited"}`), 0o644); err != nil {
  92. t.Fatal(err)
  93. }
  94. served := `{"name":"Test Board","vendorProductId":305419896,"menus":[]}`
  95. stubHTTPGet(t, func(url string) ([]byte, int, error) {
  96. if strings.Contains(url, "/v3/") {
  97. return []byte(served), http.StatusOK, nil
  98. }
  99. return nil, http.StatusNotFound, errors.New("404")
  100. })
  101. cmd := NewKeyboardFetchCmd()
  102. var out, errOut strings.Builder
  103. cmd.SetOut(&out)
  104. cmd.SetErr(&errOut)
  105. cmd.SetArgs([]string{"--force"})
  106. if err := cmd.Execute(); err != nil {
  107. t.Fatalf("keyboard fetch --force error = %v (stderr %q)", err, errOut.String())
  108. }
  109. if !strings.Contains(out.String(), "Replaced definition") {
  110. t.Errorf("stdout = %q, want it to say the definition was replaced", out.String())
  111. }
  112. entries, err := os.ReadDir(dir)
  113. if err != nil {
  114. t.Fatal(err)
  115. }
  116. if len(entries) != 1 {
  117. t.Fatalf("definitions dir = %v, want the one file replaced in place", entries)
  118. }
  119. got, readErr := os.ReadFile(filepath.Join(dir, entries[0].Name()))
  120. if readErr != nil {
  121. t.Fatal(readErr)
  122. }
  123. if string(got) != served {
  124. t.Errorf("stored file = %q, want what the server served (%q)", got, served)
  125. }
  126. }
  127. // A definition for a different board is not a reason to refuse: the fetch is for
  128. // this one, and that one is still worth having alongside.
  129. func TestFetchStoresAlongsideAnotherBoardsDefinition(t *testing.T) {
  130. dir := t.TempDir()
  131. t.Cleanup(definitionFlagRestore(t))
  132. t.Cleanup(forceDefinitionsDir(t, dir))
  133. t.Cleanup(forceFetchRestore(t))
  134. stubPrepareTarget(t, 0x1234, 0x5678)
  135. if err := os.WriteFile(filepath.Join(dir, "other.json"),
  136. []byte(`{"name":"Other","vendorId":"0x1111","productId":"0x2222"}`), 0o644); err != nil {
  137. t.Fatal(err)
  138. }
  139. served := `{"name":"Test Board","vendorProductId":305419896,"menus":[]}`
  140. stubHTTPGet(t, func(string) ([]byte, int, error) {
  141. return []byte(served), http.StatusOK, nil
  142. })
  143. cmd := NewKeyboardFetchCmd()
  144. var out, errOut strings.Builder
  145. cmd.SetOut(&out)
  146. cmd.SetErr(&errOut)
  147. if err := cmd.Execute(); err != nil {
  148. t.Fatalf("keyboard fetch error = %v (stderr %q)", err, errOut.String())
  149. }
  150. entries, err := os.ReadDir(dir)
  151. if err != nil {
  152. t.Fatal(err)
  153. }
  154. if len(entries) != 2 {
  155. t.Errorf("definitions dir = %v, want the other board's file kept alongside", entries)
  156. }
  157. }
  158. // The vendor issues one vendor and product ID to two models, so a directory can
  159. // hold two files describing the same board. --force has to leave one, not two:
  160. // which of them is read would otherwise depend on the order the directory comes
  161. // back in.
  162. func TestFetchForceLeavesOneFileForABoardWithTwo(t *testing.T) {
  163. dir := t.TempDir()
  164. t.Cleanup(definitionFlagRestore(t))
  165. t.Cleanup(forceDefinitionsDir(t, dir))
  166. t.Cleanup(forceFetchRestore(t))
  167. stubPrepareTarget(t, 0x320F, 0x5055)
  168. for _, name := range []string{"first.json", "second.json"} {
  169. body := `{"name":"Twin","vendorId":"0x320F","productId":"0x5055","menus":[]}`
  170. if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
  171. t.Fatal(err)
  172. }
  173. }
  174. served := `{"name":"Twin","vendorProductId":839864405,"menus":[]}`
  175. stubHTTPGet(t, func(string) ([]byte, int, error) {
  176. return []byte(served), http.StatusOK, nil
  177. })
  178. cmd := NewKeyboardFetchCmd()
  179. var out, errOut strings.Builder
  180. cmd.SetOut(&out)
  181. cmd.SetErr(&errOut)
  182. cmd.SetArgs([]string{"--force"})
  183. if err := cmd.Execute(); err != nil {
  184. t.Fatalf("keyboard fetch --force error = %v (stderr %q)", err, errOut.String())
  185. }
  186. entries, err := os.ReadDir(dir)
  187. if err != nil {
  188. t.Fatal(err)
  189. }
  190. if len(entries) != 1 {
  191. names := make([]string, 0, len(entries))
  192. for _, e := range entries {
  193. names = append(names, e.Name())
  194. }
  195. t.Errorf("definitions dir = %v, want exactly one file for the board", names)
  196. }
  197. }
  198. // An unknown board is answered with a web page and a success status, so the
  199. // fetch must not store it and must say what happened instead.
  200. func TestFetchDefinitionRejectsAPageThatIsNotADefinition(t *testing.T) {
  201. dir := t.TempDir()
  202. t.Cleanup(definitionFlagRestore(t))
  203. t.Cleanup(forceDefinitionsDir(t, dir))
  204. stubPrepareTarget(t, 0x1234, 0x5678)
  205. stubHTTPGet(t, func(string) ([]byte, int, error) {
  206. return []byte("<!doctype html><html><head><title>VIA</title></head></html>"), http.StatusOK, nil
  207. })
  208. cmd := NewKeyboardFetchCmd()
  209. var out, errOut strings.Builder
  210. cmd.SetOut(&out)
  211. cmd.SetErr(&errOut)
  212. err := cmd.Execute()
  213. if err == nil {
  214. t.Fatal("keyboard fetch = nil error, want a failure for a non-definition")
  215. }
  216. if !strings.Contains(err.Error(), "no definition") {
  217. t.Errorf("error = %q, want it to say there is no definition", err)
  218. }
  219. entries, _ := os.ReadDir(dir)
  220. if len(entries) != 0 {
  221. t.Errorf("definitions dir = %v, want nothing stored", entries)
  222. }
  223. }
  224. // A definition for another board must not be used, and saying so is the whole
  225. // point of carrying the identifiers.
  226. func TestDefinitionFlagRejectsTheWrongBoard(t *testing.T) {
  227. dir := t.TempDir()
  228. path := filepath.Join(dir, "other.json")
  229. if err := os.WriteFile(path, []byte(`{"name":"Other","vendorId":"0x1111","productId":"0x2222"}`), 0o600); err != nil {
  230. t.Fatal(err)
  231. }
  232. t.Cleanup(definitionFlagRestore(t))
  233. definitionFlag = path
  234. _, _, err := resolveCatalog(stubTargetData(0x1234, 0x5678))
  235. if err == nil {
  236. t.Fatal("resolveCatalog() = nil error, want a rejection for another board")
  237. }
  238. if !strings.Contains(err.Error(), "0x1111") {
  239. t.Errorf("error = %q, want it to name the board the file is for", err)
  240. }
  241. }
  242. // A definition in the data directory is used for its board without a flag.
  243. func TestResolveCatalogPrefersTheFileForTheBoard(t *testing.T) {
  244. dir := t.TempDir()
  245. if err := os.WriteFile(filepath.Join(dir, "b.json"), []byte(`{
  246. "name":"From File","vendorId":"0x1234","productId":"0x5678",
  247. "menus":[{"label":"Lighting","content":[{"label":"Backlight","content":[
  248. {"label":"Effect","type":"dropdown","content":["id_qmk_rgb_matrix_effect",3,2],
  249. "options":[["Only One",4]]}]}]}]}`), 0o600); err != nil {
  250. t.Fatal(err)
  251. }
  252. t.Cleanup(definitionFlagRestore(t))
  253. t.Cleanup(forceDefinitionsDir(t, dir))
  254. catalog, source, err := resolveCatalog(stubTargetData(0x1234, 0x5678))
  255. if err != nil {
  256. t.Fatalf("resolveCatalog() error = %v", err)
  257. }
  258. if id, ok := catalog.EffectID(3, "Only One"); !ok || id != 4 {
  259. t.Errorf("EffectID(rgb_matrix, \"Only One\") = %d, %t, want 4, true", id, ok)
  260. }
  261. if !strings.Contains(source, "b.json") {
  262. t.Errorf("source = %q, want the file it came from", source)
  263. }
  264. }
  265. // The Impact 80's definition is built into the binary, because `go install`
  266. // delivers a binary and no data directory, and this board's file cannot be
  267. // fetched. It is still the vendor's file and not a hand-written list, so it is
  268. // reported as the file it is and stays overridable.
  269. func TestResolveCatalogFallsBackToTheBuiltInDefinition(t *testing.T) {
  270. dir := t.TempDir()
  271. t.Cleanup(definitionFlagRestore(t))
  272. t.Cleanup(forceDefinitionsDir(t, dir))
  273. catalog, source, err := resolveCatalog(stubTargetData(0x36B0, 0x309F))
  274. if err != nil {
  275. t.Fatalf("resolveCatalog() error = %v", err)
  276. }
  277. if catalog == nil {
  278. t.Fatal("resolveCatalog() = nil, want the built-in definition: go install ships no data directory")
  279. }
  280. if want := "definitions/impact80.json"; source != want {
  281. t.Errorf("source = %q, want %q: it is the shipped file, not a path in an install", source, want)
  282. }
  283. if id, ok := catalog.EffectID(3, "riverflow"); !ok || id != 45 {
  284. t.Errorf("EffectID(3, \"riverflow\") = %d, %t, want 45, true", id, ok)
  285. }
  286. }
  287. // A file in a definitions directory wins over the copy built into the binary, or
  288. // the shipped file could never be corrected without a rebuild. This is the
  289. // property that keeps the built-in copy from being a second source of a name:
  290. // it is consulted last, and only when nothing else has one.
  291. func TestDefinitionsDirectoryOverridesTheBuiltInFile(t *testing.T) {
  292. dir := t.TempDir()
  293. t.Cleanup(definitionFlagRestore(t))
  294. t.Cleanup(forceDefinitionsDir(t, dir))
  295. replacement := `{"name":"Impact 80","vendorId":"0x36B0","productId":"0x309F",
  296. "menus":[{"label":"Lighting","content":[{"label":"Backlight","content":[
  297. {"label":"Effect","type":"dropdown","content":["id_qmk_rgb_matrix_effect",3,2],
  298. "options":[["only one",7]]}]}]}]}`
  299. if err := os.WriteFile(filepath.Join(dir, "impact80.json"), []byte(replacement), 0o644); err != nil {
  300. t.Fatal(err)
  301. }
  302. catalog, source, err := resolveCatalog(stubTargetData(0x36B0, 0x309F))
  303. if err != nil {
  304. t.Fatalf("resolveCatalog() error = %v", err)
  305. }
  306. if source != filepath.Join(dir, "impact80.json") {
  307. t.Errorf("source = %q, want the file in the directory", source)
  308. }
  309. if id, ok := catalog.EffectID(3, "riverflow"); ok {
  310. t.Errorf("EffectID(3, \"riverflow\") = %d, true, want not found: the placed file replaces the built-in one", id)
  311. }
  312. if id, ok := catalog.EffectID(3, "only one"); !ok || id != 7 {
  313. t.Errorf("EffectID(3, \"only one\") = %d, %t, want 7, true", id, ok)
  314. }
  315. }
  316. // An unknown board with no file has no catalog at all, which the commands
  317. // already report rather than guessing.
  318. func TestResolveCatalogHasNothingForAnUnknownBoard(t *testing.T) {
  319. dir := t.TempDir()
  320. t.Cleanup(definitionFlagRestore(t))
  321. t.Cleanup(forceDefinitionsDir(t, dir))
  322. catalog, source, err := resolveCatalog(stubTargetData(0x1111, 0x2222))
  323. if err != nil {
  324. t.Fatalf("resolveCatalog() error = %v", err)
  325. }
  326. if catalog != nil || source != "" {
  327. t.Errorf("resolveCatalog() = %v, %q, want nil, \"\"", catalog, source)
  328. }
  329. }
  330. // definitionFlagRestore resets --definition and returns a cleanup that restores
  331. // it, so a test can use the flag without leaking it into the next one.
  332. func definitionFlagRestore(t *testing.T) func() {
  333. t.Helper()
  334. original := definitionFlag
  335. return func() { definitionFlag = original }
  336. }
  337. // forceFetchRestore resets --force, so a test that sets it does not leak it into
  338. // the next one.
  339. func forceFetchRestore(t *testing.T) func() {
  340. t.Helper()
  341. original := forceFetch
  342. return func() { forceFetch = original }
  343. }
  344. // forceDefinitionsDir points the data directory at a test directory. The tool
  345. // looks for it next to the executable and then in the working directory, so a
  346. // test that wants its own has to make that lookup find it.
  347. func forceDefinitionsDir(t *testing.T, dir string) func() {
  348. t.Helper()
  349. original := definitionsDirOverride
  350. definitionsDirOverride = dir
  351. return func() { definitionsDirOverride = original }
  352. }
  353. // stubHTTPGet answers every request from a function instead of a network.
  354. func stubHTTPGet(t *testing.T, fn func(url string) ([]byte, int, error)) {
  355. t.Helper()
  356. original := httpGet
  357. httpGet = fn
  358. t.Cleanup(func() { httpGet = original })
  359. }
  360. // stubPrepareTarget makes the connected keyboard a fixed one, so a test does
  361. // not need hardware to describe a board.
  362. func stubPrepareTarget(t *testing.T, vendorID, productID uint16) {
  363. t.Helper()
  364. original := prepareTarget
  365. prepareTarget = func(string) (targetDeviceData, error) { return stubTargetData(vendorID, productID), nil }
  366. t.Cleanup(func() { prepareTarget = original })
  367. }
  368. // stubTargetData is a target for a board, for the catalog lookup tests.
  369. func stubTargetData(vendorID, productID uint16) targetDeviceData {
  370. return targetDeviceData{
  371. Device: intdevice.Device{VendorID: vendorID, ProductID: productID},
  372. Display: map[uint16]string{},
  373. }
  374. }
  375. // keyboard definitions is structured data, so --json has to reach it like every other
  376. // listing command; text by default is no excuse for a missing machine shape. The
  377. // definitions built into the binary are listed beside the ones in the user
  378. // directory, and each carries which of the two it is.
  379. func TestDefinitionListHonoursTheJSONFlag(t *testing.T) {
  380. dir := t.TempDir()
  381. if err := os.WriteFile(filepath.Join(dir, "b.json"), []byte(`{"name":"Test Board","vendorId":"0x1234","productId":"0x5678"}`), 0o600); err != nil {
  382. t.Fatal(err)
  383. }
  384. t.Cleanup(forceDefinitionsDir(t, dir))
  385. var out, errOut strings.Builder
  386. cmd := NewKeyboardDefinitionsCmd()
  387. cmd.SetOut(&out)
  388. cmd.SetErr(&errOut)
  389. withJSON(t)
  390. if err := cmd.Execute(); err != nil {
  391. t.Fatalf("keyboard definitions --json error = %v", err)
  392. }
  393. var payload struct {
  394. Definitions []struct {
  395. Name string `json:"name"`
  396. VendorID string `json:"vendorId"`
  397. ProductID string `json:"productId"`
  398. Path string `json:"path"`
  399. Source string `json:"source"`
  400. } `json:"definitions"`
  401. }
  402. if err := json.Unmarshal([]byte(out.String()), &payload); err != nil {
  403. t.Fatalf("output is not JSON: %v (%q)", err, out.String())
  404. }
  405. var found *struct {
  406. Name string `json:"name"`
  407. VendorID string `json:"vendorId"`
  408. ProductID string `json:"productId"`
  409. Path string `json:"path"`
  410. Source string `json:"source"`
  411. }
  412. for i := range payload.Definitions {
  413. if payload.Definitions[i].Name == "Test Board" {
  414. found = &payload.Definitions[i]
  415. }
  416. }
  417. if found == nil {
  418. t.Fatalf("payload = %+v, want the file in the user directory", payload.Definitions)
  419. }
  420. if found.VendorID != "0x1234" || found.ProductID != "0x5678" {
  421. t.Errorf("identifiers = %s/%s, want 0x1234/0x5678", found.VendorID, found.ProductID)
  422. }
  423. if found.Source != "user" {
  424. t.Errorf("source = %q, want %q: a file in the user directory", found.Source, "user")
  425. }
  426. }
  427. // A board whose definition is built in and whose file is not in the user
  428. // directory still appears, marked as built-in. Without it a listing would say the
  429. // tool has no definition for the board it is driving.
  430. func TestDefinitionListShowsTheBuiltInDefinitions(t *testing.T) {
  431. t.Cleanup(forceDefinitionsDir(t, t.TempDir()))
  432. var out, errOut strings.Builder
  433. cmd := NewKeyboardDefinitionsCmd()
  434. cmd.SetOut(&out)
  435. cmd.SetErr(&errOut)
  436. withJSON(t)
  437. if err := cmd.Execute(); err != nil {
  438. t.Fatalf("keyboard definitions --json error = %v", err)
  439. }
  440. var payload struct {
  441. Definitions []struct {
  442. Name string `json:"name"`
  443. Source string `json:"source"`
  444. } `json:"definitions"`
  445. }
  446. if err := json.Unmarshal([]byte(out.String()), &payload); err != nil {
  447. t.Fatalf("output is not JSON: %v (%q)", err, out.String())
  448. }
  449. if len(payload.Definitions) == 0 {
  450. t.Fatal("definitions = none, want the file built into the binary")
  451. }
  452. for _, d := range payload.Definitions {
  453. if d.Source != "built-in" {
  454. t.Errorf("%s source = %q, want %q", d.Name, d.Source, "built-in")
  455. }
  456. }
  457. }
  458. // A file in the user directory that describes a board the binary also carries
  459. // overrides it, and the listing says so, because the two files disagree and only
  460. // one of them is read.
  461. func TestDefinitionListReportsThatAUserFileShadowsABuiltInOne(t *testing.T) {
  462. dir := t.TempDir()
  463. replacement := `{"name":"Impact 80","vendorId":"0x36B0","productId":"0x309F"}`
  464. if err := os.WriteFile(filepath.Join(dir, "impact80.json"), []byte(replacement), 0o600); err != nil {
  465. t.Fatal(err)
  466. }
  467. t.Cleanup(forceDefinitionsDir(t, dir))
  468. var out, errOut strings.Builder
  469. cmd := NewKeyboardDefinitionsCmd()
  470. cmd.SetOut(&out)
  471. cmd.SetErr(&errOut)
  472. if err := cmd.Execute(); err != nil {
  473. t.Fatalf("keyboard definitions error = %v", err)
  474. }
  475. if !strings.Contains(out.String(), "shadows the built-in copy") {
  476. t.Errorf("stdout = %q, want it to say the user file overrides the built-in one", out.String())
  477. }
  478. }
  479. func TestDefinitionListPrintsTextByDefault(t *testing.T) {
  480. dir := t.TempDir()
  481. if err := os.WriteFile(filepath.Join(dir, "b.json"), []byte(`{"name":"Test Board","vendorId":"0x1234","productId":"0x5678"}`), 0o600); err != nil {
  482. t.Fatal(err)
  483. }
  484. t.Cleanup(forceDefinitionsDir(t, dir))
  485. var out, errOut strings.Builder
  486. cmd := NewKeyboardDefinitionsCmd()
  487. cmd.SetOut(&out)
  488. cmd.SetErr(&errOut)
  489. if err := cmd.Execute(); err != nil {
  490. t.Fatalf("keyboard definitions error = %v", err)
  491. }
  492. var probe any
  493. if err := json.Unmarshal([]byte(out.String()), &probe); err == nil {
  494. t.Errorf("stdout = %q, want text, not JSON", out.String())
  495. }
  496. if !strings.Contains(out.String(), "Test Board") {
  497. t.Errorf("stdout = %q, want the board name", out.String())
  498. }
  499. }
  500. // writeDefinition puts a definition file into a directory, for the tests that
  501. // need a board described by one.
  502. func writeDefinition(t *testing.T, dir, name, content string) {
  503. t.Helper()
  504. if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o600); err != nil {
  505. t.Fatal(err)
  506. }
  507. }
  508. // A names file is JSON, carries the board's vendor and product ID, and has no
  509. // menus — so read as a definition it parses, and the definitions directory is
  510. // searched before the files built into the binary. A names file sitting there
  511. // would therefore take the board's place and leave the tool with no names at all.
  512. // It declares what it is, and that declaration is what this test checks.
  513. func TestNamesFileInTheDefinitionsDirectoryIsNotADefinition(t *testing.T) {
  514. names := generateSetup(t)
  515. body := `{"kind":"names","name":"Impact 80","vendorId":"0x36B0","productId":"0x309F",
  516. "channels":{"rgb_matrix":{"7":"my_chevron"}}}`
  517. if err := os.WriteFile(filepath.Join(definitionsPath(), "oops.json"), []byte(body), 0o644); err != nil {
  518. t.Fatal(err)
  519. }
  520. _ = names
  521. // The board's real definition, built into the binary, still stands.
  522. catalog, _, err := resolveCatalogFor(0x36B0, 0x309F)
  523. if err != nil {
  524. t.Fatalf("resolveCatalogFor() error = %v", err)
  525. }
  526. if catalog == nil {
  527. t.Fatal("resolveCatalogFor() = nil catalog, want the built-in definition")
  528. }
  529. if got := len(catalog.Effects(intvia.ChannelRgbMatrix)); got != 46 {
  530. t.Errorf("effects on rgb_matrix = %d, want the definition's 46", got)
  531. }
  532. // And the names file is still read for the name it carries, from its own
  533. // directory.
  534. if err := os.MkdirAll(namesPath(), 0o755); err != nil {
  535. t.Fatal(err)
  536. }
  537. if err := os.WriteFile(filepath.Join(namesPath(), "mine.json"), []byte(body), 0o644); err != nil {
  538. t.Fatal(err)
  539. }
  540. merged, _, err := resolveCatalogFor(0x36B0, 0x309F)
  541. if err != nil {
  542. t.Fatalf("resolveCatalogFor() error = %v", err)
  543. }
  544. if got := merged.EffectName(intvia.ChannelRgbMatrix, 7); got != "my_chevron" {
  545. t.Errorf("EffectName(7) = %q, want the user's name from the names directory", got)
  546. }
  547. }