definition_test.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  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. )
  12. func TestFetchDefinitionStoresTheFileForTheBoard(t *testing.T) {
  13. dir := t.TempDir()
  14. t.Cleanup(definitionFlagRestore(t))
  15. t.Cleanup(forceDefinitionsDir(t, dir))
  16. stubPrepareTarget(t, 0x1234, 0x5678)
  17. served := `{"name":"Test Board","vendorProductId":305419896,"menus":[]}`
  18. var requested []string
  19. stubHTTPGet(t, func(url string) ([]byte, int, error) {
  20. requested = append(requested, url)
  21. if strings.Contains(url, "/v3/") {
  22. return []byte(served), http.StatusOK, nil
  23. }
  24. return nil, http.StatusNotFound, errors.New("404")
  25. })
  26. cmd := NewDefinitionCmd()
  27. var out, errOut strings.Builder
  28. cmd.SetOut(&out)
  29. cmd.SetErr(&errOut)
  30. cmd.SetArgs([]string{"fetch"})
  31. if err := cmd.Execute(); err != nil {
  32. t.Fatalf("definition 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. // An unknown board is answered with a web page and a success status, so the
  49. // fetch must not store it and must say what happened instead.
  50. func TestFetchDefinitionRejectsAPageThatIsNotADefinition(t *testing.T) {
  51. dir := t.TempDir()
  52. t.Cleanup(definitionFlagRestore(t))
  53. t.Cleanup(forceDefinitionsDir(t, dir))
  54. stubPrepareTarget(t, 0x1234, 0x5678)
  55. stubHTTPGet(t, func(string) ([]byte, int, error) {
  56. return []byte("<!doctype html><html><head><title>VIA</title></head></html>"), http.StatusOK, nil
  57. })
  58. cmd := NewDefinitionCmd()
  59. var out, errOut strings.Builder
  60. cmd.SetOut(&out)
  61. cmd.SetErr(&errOut)
  62. cmd.SetArgs([]string{"fetch"})
  63. err := cmd.Execute()
  64. if err == nil {
  65. t.Fatal("definition fetch = nil error, want a failure for a non-definition")
  66. }
  67. if !strings.Contains(err.Error(), "no definition") {
  68. t.Errorf("error = %q, want it to say there is no definition", err)
  69. }
  70. entries, _ := os.ReadDir(dir)
  71. if len(entries) != 0 {
  72. t.Errorf("definitions dir = %v, want nothing stored", entries)
  73. }
  74. }
  75. // A definition for another board must not be used, and saying so is the whole
  76. // point of carrying the identifiers.
  77. func TestDefinitionFlagRejectsTheWrongBoard(t *testing.T) {
  78. dir := t.TempDir()
  79. path := filepath.Join(dir, "other.json")
  80. if err := os.WriteFile(path, []byte(`{"name":"Other","vendorId":"0x1111","productId":"0x2222"}`), 0o600); err != nil {
  81. t.Fatal(err)
  82. }
  83. t.Cleanup(definitionFlagRestore(t))
  84. definitionFlag = path
  85. _, _, err := resolveCatalog(stubTargetData(0x1234, 0x5678))
  86. if err == nil {
  87. t.Fatal("resolveCatalog() = nil error, want a rejection for another board")
  88. }
  89. if !strings.Contains(err.Error(), "0x1111") {
  90. t.Errorf("error = %q, want it to name the board the file is for", err)
  91. }
  92. }
  93. // A definition in the data directory is used for its board without a flag.
  94. func TestResolveCatalogPrefersTheFileForTheBoard(t *testing.T) {
  95. dir := t.TempDir()
  96. if err := os.WriteFile(filepath.Join(dir, "b.json"), []byte(`{
  97. "name":"From File","vendorId":"0x1234","productId":"0x5678",
  98. "menus":[{"label":"Lighting","content":[{"label":"Backlight","content":[
  99. {"label":"Effect","type":"dropdown","content":["id_qmk_rgb_matrix_effect",3,2],
  100. "options":[["Only One",4]]}]}]}]}`), 0o600); err != nil {
  101. t.Fatal(err)
  102. }
  103. t.Cleanup(definitionFlagRestore(t))
  104. t.Cleanup(forceDefinitionsDir(t, dir))
  105. catalog, source, err := resolveCatalog(stubTargetData(0x1234, 0x5678))
  106. if err != nil {
  107. t.Fatalf("resolveCatalog() error = %v", err)
  108. }
  109. if id, ok := catalog.EffectID(3, "Only One"); !ok || id != 4 {
  110. t.Errorf("EffectID(rgb_matrix, \"Only One\") = %d, %t, want 4, true", id, ok)
  111. }
  112. if !strings.Contains(source, "b.json") {
  113. t.Errorf("source = %q, want the file it came from", source)
  114. }
  115. }
  116. // Nothing is compiled in, so a board the data directory does not describe has
  117. // no catalog at all. That is the point of one source: a hand-written list and a
  118. // vendor file describing the same board are two places to update a name.
  119. func TestResolveCatalogHasNothingCompiledInAsAFallback(t *testing.T) {
  120. dir := t.TempDir()
  121. t.Cleanup(definitionFlagRestore(t))
  122. t.Cleanup(forceDefinitionsDir(t, dir))
  123. catalog, source, err := resolveCatalog(stubTargetData(0x36B0, 0x309F))
  124. if err != nil {
  125. t.Fatalf("resolveCatalog() error = %v", err)
  126. }
  127. if catalog != nil || source != "" {
  128. t.Errorf("resolveCatalog() = %v, %q, want nil, \"\"", catalog, source)
  129. }
  130. }
  131. // An unknown board with no file has no catalog at all, which the commands
  132. // already report rather than guessing.
  133. func TestResolveCatalogHasNothingForAnUnknownBoard(t *testing.T) {
  134. dir := t.TempDir()
  135. t.Cleanup(definitionFlagRestore(t))
  136. t.Cleanup(forceDefinitionsDir(t, dir))
  137. catalog, source, err := resolveCatalog(stubTargetData(0x1111, 0x2222))
  138. if err != nil {
  139. t.Fatalf("resolveCatalog() error = %v", err)
  140. }
  141. if catalog != nil || source != "" {
  142. t.Errorf("resolveCatalog() = %v, %q, want nil, \"\"", catalog, source)
  143. }
  144. }
  145. // definitionFlagRestore resets --definition and returns a cleanup that restores
  146. // it, so a test can use the flag without leaking it into the next one.
  147. func definitionFlagRestore(t *testing.T) func() {
  148. t.Helper()
  149. original := definitionFlag
  150. return func() { definitionFlag = original }
  151. }
  152. // forceDefinitionsDir points the data directory at a test directory. The tool
  153. // looks for it next to the executable and then in the working directory, so a
  154. // test that wants its own has to make that lookup find it.
  155. func forceDefinitionsDir(t *testing.T, dir string) func() {
  156. t.Helper()
  157. original := definitionsDirOverride
  158. definitionsDirOverride = dir
  159. return func() { definitionsDirOverride = original }
  160. }
  161. // stubHTTPGet answers every request from a function instead of a network.
  162. func stubHTTPGet(t *testing.T, fn func(url string) ([]byte, int, error)) {
  163. t.Helper()
  164. original := httpGet
  165. httpGet = fn
  166. t.Cleanup(func() { httpGet = original })
  167. }
  168. // stubPrepareTarget makes the connected keyboard a fixed one, so a test does
  169. // not need hardware to describe a board.
  170. func stubPrepareTarget(t *testing.T, vendorID, productID uint16) {
  171. t.Helper()
  172. original := prepareTarget
  173. prepareTarget = func() (targetDeviceData, error) { return stubTargetData(vendorID, productID), nil }
  174. t.Cleanup(func() { prepareTarget = original })
  175. }
  176. // stubTargetData is a target for a board, for the catalog lookup tests.
  177. func stubTargetData(vendorID, productID uint16) targetDeviceData {
  178. return targetDeviceData{
  179. Device: intdevice.Device{VendorID: vendorID, ProductID: productID},
  180. Display: map[uint16]string{},
  181. }
  182. }
  183. // definition list is structured data, so --json has to reach it like every other
  184. // listing command; text by default is no excuse for a missing machine shape.
  185. func TestDefinitionListHonoursTheJSONFlag(t *testing.T) {
  186. dir := t.TempDir()
  187. if err := os.WriteFile(filepath.Join(dir, "b.json"), []byte(`{"name":"Test Board","vendorId":"0x1234","productId":"0x5678"}`), 0o600); err != nil {
  188. t.Fatal(err)
  189. }
  190. t.Cleanup(forceDefinitionsDir(t, dir))
  191. var out, errOut strings.Builder
  192. cmd := NewDefinitionCmd()
  193. cmd.SetOut(&out)
  194. cmd.SetErr(&errOut)
  195. cmd.SetArgs([]string{"list"})
  196. withJSON(t)
  197. if err := cmd.Execute(); err != nil {
  198. t.Fatalf("definition list --json error = %v", err)
  199. }
  200. var payload struct {
  201. Definitions []struct {
  202. Name string `json:"name"`
  203. VendorID string `json:"vendorId"`
  204. ProductID string `json:"productId"`
  205. Path string `json:"path"`
  206. } `json:"definitions"`
  207. }
  208. if err := json.Unmarshal([]byte(out.String()), &payload); err != nil {
  209. t.Fatalf("output is not JSON: %v (%q)", err, out.String())
  210. }
  211. if len(payload.Definitions) != 1 || payload.Definitions[0].Name != "Test Board" {
  212. t.Errorf("payload = %+v, want the one definition", payload.Definitions)
  213. }
  214. if payload.Definitions[0].VendorID != "0x1234" || payload.Definitions[0].ProductID != "0x5678" {
  215. t.Errorf("identifiers = %s/%s, want 0x1234/0x5678",
  216. payload.Definitions[0].VendorID, payload.Definitions[0].ProductID)
  217. }
  218. }
  219. func TestDefinitionListPrintsTextByDefault(t *testing.T) {
  220. dir := t.TempDir()
  221. if err := os.WriteFile(filepath.Join(dir, "b.json"), []byte(`{"name":"Test Board","vendorId":"0x1234","productId":"0x5678"}`), 0o600); err != nil {
  222. t.Fatal(err)
  223. }
  224. t.Cleanup(forceDefinitionsDir(t, dir))
  225. var out, errOut strings.Builder
  226. cmd := NewDefinitionCmd()
  227. cmd.SetOut(&out)
  228. cmd.SetErr(&errOut)
  229. cmd.SetArgs([]string{"list"})
  230. if err := cmd.Execute(); err != nil {
  231. t.Fatalf("definition list error = %v", err)
  232. }
  233. var probe any
  234. if err := json.Unmarshal([]byte(out.String()), &probe); err == nil {
  235. t.Errorf("stdout = %q, want text, not JSON", out.String())
  236. }
  237. if !strings.Contains(out.String(), "Test Board") {
  238. t.Errorf("stdout = %q, want the board name", out.String())
  239. }
  240. }
  241. // writeDefinition puts a definition file into a directory, for the tests that
  242. // need a board described by one.
  243. func writeDefinition(t *testing.T, dir, name, content string) {
  244. t.Helper()
  245. if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o600); err != nil {
  246. t.Fatal(err)
  247. }
  248. }