| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 |
- package main
- import (
- "encoding/json"
- "errors"
- "net/http"
- "os"
- "path/filepath"
- "strings"
- "testing"
- intdevice "netdome.biz/paul/qmk-rgb/internal/device"
- )
- func TestFetchDefinitionStoresTheFileForTheBoard(t *testing.T) {
- dir := t.TempDir()
- t.Cleanup(definitionFlagRestore(t))
- t.Cleanup(forceDefinitionsDir(t, dir))
- stubPrepareTarget(t, 0x1234, 0x5678)
- served := `{"name":"Test Board","vendorProductId":305419896,"menus":[]}`
- var requested []string
- stubHTTPGet(t, func(url string) ([]byte, int, error) {
- requested = append(requested, url)
- if strings.Contains(url, "/v3/") {
- return []byte(served), http.StatusOK, nil
- }
- return nil, http.StatusNotFound, errors.New("404")
- })
- cmd := NewDefinitionCmd()
- var out, errOut strings.Builder
- cmd.SetOut(&out)
- cmd.SetErr(&errOut)
- cmd.SetArgs([]string{"fetch"})
- if err := cmd.Execute(); err != nil {
- t.Fatalf("definition fetch error = %v (stderr %q)", err, errOut.String())
- }
- if len(requested) != 1 || !strings.Contains(requested[0], "/v3/305419896.json") {
- t.Errorf("requested = %v, want one v3 URL for vendorProductId 305419896", requested)
- }
- if !strings.Contains(out.String(), "Test Board") {
- t.Errorf("stdout = %q, want the board name", out.String())
- }
- entries, err := os.ReadDir(dir)
- if err != nil {
- t.Fatal(err)
- }
- if len(entries) != 1 || !strings.HasSuffix(entries[0].Name(), ".json") {
- t.Fatalf("definitions dir = %v, want one JSON file", entries)
- }
- }
- // An unknown board is answered with a web page and a success status, so the
- // fetch must not store it and must say what happened instead.
- func TestFetchDefinitionRejectsAPageThatIsNotADefinition(t *testing.T) {
- dir := t.TempDir()
- t.Cleanup(definitionFlagRestore(t))
- t.Cleanup(forceDefinitionsDir(t, dir))
- stubPrepareTarget(t, 0x1234, 0x5678)
- stubHTTPGet(t, func(string) ([]byte, int, error) {
- return []byte("<!doctype html><html><head><title>VIA</title></head></html>"), http.StatusOK, nil
- })
- cmd := NewDefinitionCmd()
- var out, errOut strings.Builder
- cmd.SetOut(&out)
- cmd.SetErr(&errOut)
- cmd.SetArgs([]string{"fetch"})
- err := cmd.Execute()
- if err == nil {
- t.Fatal("definition fetch = nil error, want a failure for a non-definition")
- }
- if !strings.Contains(err.Error(), "no definition") {
- t.Errorf("error = %q, want it to say there is no definition", err)
- }
- entries, _ := os.ReadDir(dir)
- if len(entries) != 0 {
- t.Errorf("definitions dir = %v, want nothing stored", entries)
- }
- }
- // A definition for another board must not be used, and saying so is the whole
- // point of carrying the identifiers.
- func TestDefinitionFlagRejectsTheWrongBoard(t *testing.T) {
- dir := t.TempDir()
- path := filepath.Join(dir, "other.json")
- if err := os.WriteFile(path, []byte(`{"name":"Other","vendorId":"0x1111","productId":"0x2222"}`), 0o600); err != nil {
- t.Fatal(err)
- }
- t.Cleanup(definitionFlagRestore(t))
- definitionFlag = path
- _, _, err := resolveCatalog(stubTargetData(0x1234, 0x5678))
- if err == nil {
- t.Fatal("resolveCatalog() = nil error, want a rejection for another board")
- }
- if !strings.Contains(err.Error(), "0x1111") {
- t.Errorf("error = %q, want it to name the board the file is for", err)
- }
- }
- // A definition in the data directory is used for its board without a flag.
- func TestResolveCatalogPrefersTheFileForTheBoard(t *testing.T) {
- dir := t.TempDir()
- if err := os.WriteFile(filepath.Join(dir, "b.json"), []byte(`{
- "name":"From File","vendorId":"0x1234","productId":"0x5678",
- "menus":[{"label":"Lighting","content":[{"label":"Backlight","content":[
- {"label":"Effect","type":"dropdown","content":["id_qmk_rgb_matrix_effect",3,2],
- "options":[["Only One",4]]}]}]}]}`), 0o600); err != nil {
- t.Fatal(err)
- }
- t.Cleanup(definitionFlagRestore(t))
- t.Cleanup(forceDefinitionsDir(t, dir))
- catalog, source, err := resolveCatalog(stubTargetData(0x1234, 0x5678))
- if err != nil {
- t.Fatalf("resolveCatalog() error = %v", err)
- }
- if id, ok := catalog.EffectID(3, "Only One"); !ok || id != 4 {
- t.Errorf("EffectID(rgb_matrix, \"Only One\") = %d, %t, want 4, true", id, ok)
- }
- if !strings.Contains(source, "b.json") {
- t.Errorf("source = %q, want the file it came from", source)
- }
- }
- // The built-in catalog is the last resort, so a board the tool knows about
- // keeps working with no definition file anywhere.
- func TestResolveCatalogFallsBackToTheBuiltInCatalog(t *testing.T) {
- dir := t.TempDir()
- t.Cleanup(definitionFlagRestore(t))
- t.Cleanup(forceDefinitionsDir(t, dir))
- catalog, source, err := resolveCatalog(stubTargetData(0x36B0, 0x309F))
- if err != nil {
- t.Fatalf("resolveCatalog() error = %v", err)
- }
- if catalog == nil {
- t.Fatal("resolveCatalog() = nil catalog, want the built-in Impact 80 one")
- }
- if !strings.Contains(source, "built in") {
- t.Errorf("source = %q, want it to say the catalog is built in", source)
- }
- }
- // An unknown board with no file has no catalog at all, which the commands
- // already report rather than guessing.
- func TestResolveCatalogHasNothingForAnUnknownBoard(t *testing.T) {
- dir := t.TempDir()
- t.Cleanup(definitionFlagRestore(t))
- t.Cleanup(forceDefinitionsDir(t, dir))
- catalog, source, err := resolveCatalog(stubTargetData(0x1111, 0x2222))
- if err != nil {
- t.Fatalf("resolveCatalog() error = %v", err)
- }
- if catalog != nil || source != "" {
- t.Errorf("resolveCatalog() = %v, %q, want nil, \"\"", catalog, source)
- }
- }
- // definitionFlagRestore resets --definition and returns a cleanup that restores
- // it, so a test can use the flag without leaking it into the next one.
- func definitionFlagRestore(t *testing.T) func() {
- t.Helper()
- original := definitionFlag
- return func() { definitionFlag = original }
- }
- // forceDefinitionsDir points the data directory at a test directory. The tool
- // looks for it next to the executable and then in the working directory, so a
- // test that wants its own has to make that lookup find it.
- func forceDefinitionsDir(t *testing.T, dir string) func() {
- t.Helper()
- original := definitionsDir
- definitionsDir = func() (string, error) { return dir, nil }
- return func() { definitionsDir = original }
- }
- // stubHTTPGet answers every request from a function instead of a network.
- func stubHTTPGet(t *testing.T, fn func(url string) ([]byte, int, error)) {
- t.Helper()
- original := httpGet
- httpGet = fn
- t.Cleanup(func() { httpGet = original })
- }
- // stubPrepareTarget makes the connected keyboard a fixed one, so a test does
- // not need hardware to describe a board.
- func stubPrepareTarget(t *testing.T, vendorID, productID uint16) {
- t.Helper()
- original := prepareTarget
- prepareTarget = func() (targetDeviceData, error) { return stubTargetData(vendorID, productID), nil }
- t.Cleanup(func() { prepareTarget = original })
- }
- // stubTargetData is a target for a board, for the catalog lookup tests.
- func stubTargetData(vendorID, productID uint16) targetDeviceData {
- return targetDeviceData{
- Device: intdevice.Device{VendorID: vendorID, ProductID: productID},
- Display: map[uint16]string{},
- }
- }
- // definition list is structured data, so --json has to reach it like every other
- // listing command; text by default is no excuse for a missing machine shape.
- func TestDefinitionListHonoursTheJSONFlag(t *testing.T) {
- dir := t.TempDir()
- if err := os.WriteFile(filepath.Join(dir, "b.json"), []byte(`{"name":"Test Board","vendorId":"0x1234","productId":"0x5678"}`), 0o600); err != nil {
- t.Fatal(err)
- }
- t.Cleanup(forceDefinitionsDir(t, dir))
- var out, errOut strings.Builder
- cmd := NewDefinitionCmd()
- cmd.SetOut(&out)
- cmd.SetErr(&errOut)
- cmd.SetArgs([]string{"list"})
- withJSON(t)
- if err := cmd.Execute(); err != nil {
- t.Fatalf("definition list --json error = %v", err)
- }
- var payload struct {
- Definitions []struct {
- Name string `json:"name"`
- VendorID string `json:"vendorId"`
- ProductID string `json:"productId"`
- Path string `json:"path"`
- } `json:"definitions"`
- }
- if err := json.Unmarshal([]byte(out.String()), &payload); err != nil {
- t.Fatalf("output is not JSON: %v (%q)", err, out.String())
- }
- if len(payload.Definitions) != 1 || payload.Definitions[0].Name != "Test Board" {
- t.Errorf("payload = %+v, want the one definition", payload.Definitions)
- }
- if payload.Definitions[0].VendorID != "0x1234" || payload.Definitions[0].ProductID != "0x5678" {
- t.Errorf("identifiers = %s/%s, want 0x1234/0x5678",
- payload.Definitions[0].VendorID, payload.Definitions[0].ProductID)
- }
- }
- func TestDefinitionListPrintsTextByDefault(t *testing.T) {
- dir := t.TempDir()
- if err := os.WriteFile(filepath.Join(dir, "b.json"), []byte(`{"name":"Test Board","vendorId":"0x1234","productId":"0x5678"}`), 0o600); err != nil {
- t.Fatal(err)
- }
- t.Cleanup(forceDefinitionsDir(t, dir))
- var out, errOut strings.Builder
- cmd := NewDefinitionCmd()
- cmd.SetOut(&out)
- cmd.SetErr(&errOut)
- cmd.SetArgs([]string{"list"})
- if err := cmd.Execute(); err != nil {
- t.Fatalf("definition list error = %v", err)
- }
- var probe any
- if err := json.Unmarshal([]byte(out.String()), &probe); err == nil {
- t.Errorf("stdout = %q, want text, not JSON", out.String())
- }
- if !strings.Contains(out.String(), "Test Board") {
- t.Errorf("stdout = %q, want the board name", out.String())
- }
- }
- // writeDefinition puts a definition file into a directory, for the tests that
- // need a board described by one.
- func writeDefinition(t *testing.T, dir, name, content string) {
- t.Helper()
- if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o600); err != nil {
- t.Fatal(err)
- }
- }
|