Repository navigation
Expand file tree
/
Copy pathmain_docs_test.go
More file actions
175 lines (164 loc) · 5.43 KB
/
Copy pathmain_docs_test.go
File metadata and controls
175 lines (164 loc) · 5.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
package main
import (
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
)
// siteFlagsFile is the site's flag table, generated from the real FlagSet so
// the docs cannot drift from `subenum -h` (#64).
var siteFlagsFile = filepath.Join("docs", "_includes", "flags.md")
var readmeFlagsFile = "README.md"
// siteUnreleasedFile lists what the site tags "unreleased" (#213).
var siteUnreleasedFile = filepath.Join("docs", "_data", "unreleased.yml")
// unreleasedFlags reads the `flags:` list of the site's unreleased.yml: the
// two-space "- name" items under that key, with no YAML dependency.
func unreleasedFlags(data string) map[string]bool {
names := make(map[string]bool)
inFlags := false
for _, line := range strings.Split(strings.ReplaceAll(data, "\r\n", "\n"), "\n") {
switch {
case strings.HasPrefix(line, "#") || strings.TrimSpace(line) == "":
case !strings.HasPrefix(line, " "):
inFlags = strings.TrimSpace(line) == "flags:"
case inFlags:
if name, ok := strings.CutPrefix(strings.TrimSpace(line), "- "); ok {
names[strings.TrimSpace(name)] = true
}
}
}
return names
}
// flagsMarkdown renders every flag as a Markdown table, sorted by name. Flags
// in unreleased get the site's "unreleased" tag.
func flagsMarkdown(fs *flag.FlagSet, unreleased map[string]bool) string {
var b strings.Builder
b.WriteString("<!-- Generated from subenum's flags by `make docs-flags`; do not edit. -->\n\n")
b.WriteString("| Flag | Default | Description |\n| :--- | :--- | :--- |\n")
fs.VisitAll(func(fl *flag.Flag) {
name, usage := flag.UnquoteUsage(fl)
flagCol := "-" + fl.Name
if name != "" {
flagCol += " <" + name + ">"
}
def := fl.DefValue
switch def {
case "", "0", "false":
def = "-"
// A 0 sentinel whose real default is resolved later (-attempts)
// names it in the usage text as "(default N)".
if i := strings.LastIndex(usage, "(default "); i >= 0 && strings.HasSuffix(usage, ")") {
def = usage[i+len("(default ") : len(usage)-1]
}
}
code := func(s string) string { return strings.ReplaceAll(s, "|", `\|`) }
// Description cells are Markdown: keep <domain> and *.parent literal.
text := strings.NewReplacer("|", `\|`, "<", "<", ">", ">", "*", `\*`, "_", `\_`)
tag := ""
if unreleased[fl.Name] {
tag = fmt.Sprintf(` {%% include unreleased.html flag=%q %%}`, fl.Name)
}
fmt.Fprintf(&b, "| `%s`%s | %s | %s |\n", code(flagCol), tag, text.Replace(def), text.Replace(usage))
})
return b.String()
}
// TestSiteFlagTableUpToDate fails when the site's flag table no longer
// matches the CLI. Regenerate it with `make docs-flags`.
func TestSiteFlagTableUpToDate(t *testing.T) {
_, _, fs, _ := parseFlags(nil)
data, err := os.ReadFile(siteUnreleasedFile)
if err != nil {
t.Fatal(err)
}
unreleased := unreleasedFlags(string(data))
for name := range unreleased {
if fs.Lookup(name) == nil {
t.Fatalf("%s lists -%s, which is not a CLI flag", siteUnreleasedFile, name)
}
}
want := flagsMarkdown(fs, unreleased)
if os.Getenv("UPDATE_DOCS") == "1" {
if err := os.WriteFile(siteFlagsFile, []byte(want), 0o644); err != nil { //nolint:gosec // a docs file
t.Fatal(err)
}
return
}
got, err := os.ReadFile(siteFlagsFile)
if err != nil {
t.Fatalf("%v (run `make docs-flags`)", err)
}
if strings.ReplaceAll(string(got), "\r\n", "\n") != want {
t.Fatalf("%s is out of date with the CLI flags; run `make docs-flags`", siteFlagsFile)
}
}
// TestUnreleasedFlags reads only the flags: list, ignoring comments and the
// other lists that share the file.
func TestUnreleasedFlags(t *testing.T) {
data := "# note\r\nflags:\r\n - dry-run\r\n # skipped\r\n - log-format\r\naction:\r\n - changed\r\nlibrary:\r\n - MaxDuration\r\n"
got := unreleasedFlags(data)
if len(got) != 2 || !got["dry-run"] || !got["log-format"] {
t.Fatalf("unreleasedFlags = %v, want dry-run and log-format", got)
}
if len(unreleasedFlags("flags:\naction:\n - changed\n")) != 0 {
t.Fatal("an empty flags: list should tag nothing")
}
}
// TestREADMEFlagTableCoversCLI keeps the hand-written README table complete
// when a CLI flag is added. The website table is generated, but the README is
// maintained separately for quick reference (#137).
func TestREADMEFlagTableCoversCLI(t *testing.T) {
contents, err := os.ReadFile(readmeFlagsFile) //nolint:gosec // a docs file
if err != nil {
t.Fatal(err)
}
inTable := false
tableStarted := false
documented := make(map[string]bool)
for _, line := range strings.Split(strings.ReplaceAll(string(contents), "\r\n", "\n"), "\n") {
line = strings.TrimSpace(line)
if line == "### CLI flags" {
inTable = true
continue
}
if !inTable {
continue
}
if !strings.HasPrefix(line, "|") {
if tableStarted {
break
}
continue
}
tableStarted = true
cells := strings.SplitN(line, "|", 3)
if len(cells) < 2 {
continue
}
cell := strings.TrimSpace(cells[1])
if !strings.HasPrefix(cell, "`-") {
continue
}
name := strings.TrimPrefix(cell, "`-")
if end := strings.IndexAny(name, " \t<`"); end >= 0 {
name = name[:end]
}
if name != "" {
documented[name] = true
}
}
if !inTable || !tableStarted {
t.Fatalf("%s has no CLI flags table", readmeFlagsFile)
}
_, _, fs, _ := parseFlags(nil)
var missing []string
fs.VisitAll(func(fl *flag.Flag) {
if !documented[fl.Name] {
missing = append(missing, "-"+fl.Name)
}
})
if len(missing) > 0 {
t.Fatalf("%s CLI flags table is missing flags: %s", readmeFlagsFile, strings.Join(missing, ", "))
}
}