flags_test.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. package main
  2. import (
  3. "os"
  4. "strings"
  5. "testing"
  6. "github.com/spf13/cobra"
  7. )
  8. // The --device flag accepts the 1-based number that `keyboard info` prints.
  9. // Its help text is the only place a user learns that, so it must not
  10. // describe the HID path the flag rejects.
  11. func TestDeviceFlagUsageDescribesANumberNotAPath(t *testing.T) {
  12. flag := newRootCommand().PersistentFlags().Lookup("device")
  13. if flag == nil {
  14. t.Fatal("--device flag not found")
  15. }
  16. if strings.Contains(flag.Usage, "path") {
  17. t.Errorf("--device usage = %q, must not mention a path; the flag rejects HID paths", flag.Usage)
  18. }
  19. if !strings.Contains(flag.Usage, "number") {
  20. t.Errorf("--device usage = %q, want it to say the argument is a number", flag.Usage)
  21. }
  22. }
  23. // Cobra reads a back-quoted word in a flag's usage as the value placeholder
  24. // and renders it where the type name would go, turning `--device into
  25. // `--device keyboard info`. The help must therefore contain no backticks.
  26. func TestFlagUsageHasNoValuePlaceholder(t *testing.T) {
  27. root := newRootCommand()
  28. for _, name := range []string{"device", "definition", "json"} {
  29. flag := root.PersistentFlags().Lookup(name)
  30. if flag == nil {
  31. t.Errorf("--%s is registered on the root, want it", name)
  32. continue
  33. }
  34. if strings.Contains(flag.Usage, "`") {
  35. t.Errorf("--%s usage = %q, backticks make cobra render a bogus value placeholder", name, flag.Usage)
  36. }
  37. }
  38. }
  39. // The flag usage is not the only place a user learns what --device takes. The
  40. // same promise is repeated in three documentation files, and it was wrong in
  41. // all of them at once. Pin the docs to the flag's actual contract by looking
  42. // for a value that looks like a HID path, not for the word "path" — prose
  43. // about why paths are unstable, and the rule that forbids them, must pass.
  44. func TestDocsDoNotOfferAPathValueForDevice(t *testing.T) {
  45. pathLike := []string{"/dev/", "hidraw", `\\?\hid`, "IO/HIDDevice"}
  46. for _, path := range []string{
  47. "../../README.md",
  48. "../../AGENTS.md",
  49. "../../.claude/skills/qmk-rgb/SKILL.md",
  50. } {
  51. data, err := os.ReadFile(path)
  52. if err != nil {
  53. t.Errorf("read %s: %v", path, err)
  54. continue
  55. }
  56. for _, line := range strings.Split(string(data), "\n") {
  57. if !strings.Contains(line, "--device") {
  58. continue
  59. }
  60. for _, needle := range pathLike {
  61. if strings.Contains(line, needle) {
  62. t.Errorf("%s offers a path value for --device: %q", path, strings.TrimSpace(line))
  63. }
  64. }
  65. }
  66. }
  67. }
  68. // The docs must not teach a zone name the resolver rejects. The subsystem
  69. // names are the vocabulary; a board's own channel names come from its definition file.
  70. func TestDocsDoNotTeachAZoneNameTheResolverRejects(t *testing.T) {
  71. for _, path := range []string{
  72. "../../README.md",
  73. "../../AGENTS.md",
  74. "../../.claude/skills/qmk-rgb/SKILL.md",
  75. } {
  76. data, err := os.ReadFile(path)
  77. if err != nil {
  78. t.Errorf("read %s: %v", path, err)
  79. continue
  80. }
  81. body := string(data)
  82. for _, wrong := range []string{"zone logo|backlight", "zone matrix", "zone=matrix"} {
  83. if strings.Contains(body, wrong) {
  84. t.Errorf("%s contains %q, want only the canonical zone names", path, wrong)
  85. }
  86. }
  87. }
  88. }
  89. // A usage line is the one place a command's arity is declared, and the docs
  90. // repeat it. The two drifted here: the table spelled `effect <zone> <name|index>`
  91. // with the name required, while the command reads with one argument and writes
  92. // with two. Pin the table to the line the command actually prints, and keep every
  93. // message that offers the ID form from dropping the zone — `effect 17` alone is
  94. // read as a channel named `17`, so advice in that form does not run.
  95. func TestDocsSpellTheEffectUsageAsTheCommandDoes(t *testing.T) {
  96. used := NewEffectCmd().Use
  97. if !strings.Contains(used, "[name|index]") {
  98. t.Fatalf("effect Use = %q, want it to mark the name optional", used)
  99. }
  100. want := "qmk-rgb-tool " + used
  101. readme, err := os.ReadFile("../../README.md")
  102. if err != nil {
  103. t.Fatalf("read README.md: %v", err)
  104. }
  105. // The reference table lives in a markdown table, where a pipe inside a cell is
  106. // escaped, so the line carries a backslash the usage string does not. Compare
  107. // against the unescaped form rather than making the table write a broken cell.
  108. if !strings.Contains(strings.ReplaceAll(string(readme), `\|`, "|"), want) {
  109. t.Errorf("README.md does not carry the usage line %q", want)
  110. }
  111. // The bare form is the defect, so look for exactly that and not for the
  112. // substring it shares with the correct one.
  113. for _, path := range []string{
  114. "../../README.md",
  115. "../../AGENTS.md",
  116. "../../.claude/skills/qmk-rgb/SKILL.md",
  117. "effect.go", "enable.go", "profile.go", "../../internal/rgb/catalog.go",
  118. } {
  119. data, err := os.ReadFile(path)
  120. if err != nil {
  121. t.Errorf("read %s: %v", path, err)
  122. continue
  123. }
  124. for i, line := range strings.Split(string(data), "\n") {
  125. if strings.Contains(line, "effect <index>") {
  126. t.Errorf("%s:%d points at `effect <index>`, which reads the ID as a zone: %q",
  127. path, i+1, strings.TrimSpace(line))
  128. }
  129. }
  130. }
  131. }
  132. // The zone is a positional argument now, so its help is the Long text the
  133. // command carries rather than a flag's usage string. Every subsystem name has to
  134. // be discoverable there, because a user reading `brightness --help` has nowhere
  135. // else to learn what a zone may be.
  136. func TestZoneHelpNamesTheChannelVocabulary(t *testing.T) {
  137. cmds := map[string]*cobra.Command{
  138. "effect": NewEffectCmd(),
  139. "brightness": NewBrightnessCmd(),
  140. "speed": NewSpeedCmd(),
  141. "color": NewColorCmd(),
  142. "enable": NewEnableCmd(),
  143. "disable": NewDisableCmd(),
  144. }
  145. for name, cmd := range cmds {
  146. t.Run(name, func(t *testing.T) {
  147. for _, zone := range []string{"backlight", "rgblight", "rgb_matrix", "audio", "led_matrix"} {
  148. if !strings.Contains(cmd.Long, zone) {
  149. t.Errorf("%s help does not name the subsystem %q", name, zone)
  150. }
  151. }
  152. if !strings.Contains(cmd.Long, "all") {
  153. t.Errorf("%s help does not say that all is a zone", name)
  154. }
  155. if !strings.Contains(cmd.Long, "comma") {
  156. t.Errorf("%s help does not say that several zones may be written at once", name)
  157. }
  158. // The board-supplied names are not a fixed vocabulary, so the help
  159. // must not claim they are.
  160. if strings.Contains(cmd.Long, "logo") || strings.Contains(cmd.Long, "side") {
  161. t.Errorf("%s help names channels a board supplies", name)
  162. }
  163. })
  164. }
  165. }