-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathreadme_write_capability_test.go
More file actions
271 lines (241 loc) · 9.59 KB
/
Copy pathreadme_write_capability_test.go
File metadata and controls
271 lines (241 loc) · 9.59 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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
package gometadata
// readme_write_capability_test.go — regression guard for task #193.
//
// TestREADMEWriteCapabilityMatchesCode enforces the doc↔code invariant:
// every format for which format.SupportsWrite returns true must NOT appear as
// read-only (Write column "No" or in any "ErrWriteNotSupported" enumeration)
// in README.md.
//
// Motivation: this divergence already regressed once (Sprint 9) and was
// discovered only during release hardening. A test that reads the README and
// correlates it with SupportsWrite ensures the divergence cannot silently
// reappear after future un-gates or re-gates.
//
// Design:
// - The test locates the "Supported formats" table inside README.md by
// searching for the header row (| Format | Extension(s) | ...).
// - It parses each data row to extract the format name (column 0) and the
// Write column (column 3).
// - For each format whose Write column contains "No" (case-insensitive), it
// maps the display name back to a FormatID and asserts SupportsWrite==false.
// - It also scans for any prose line that lists format names alongside the
// text "ErrWriteNotSupported" and asserts those formats also have
// SupportsWrite==false in code.
// - If the "Supported formats" table is not found, the test FAILS rather than
// skipping — t.Skip is forbidden by project policy.
//
// Robustness:
// - Anchors on format name tokens, not on line numbers or column counts, so
// minor reformatting does not break the test.
// - Footnote markers (¹ ² ³ ⁴ etc.) are stripped before comparison so "Yes²"
// matches the same as "Yes".
import (
"bufio"
"os"
"strings"
"testing"
"unicode"
"github.com/FlavioCFOliveira/GoMetadata/format"
)
// readmeFormatDisplayNames maps the human-readable name used in the README
// table to the corresponding FormatID constant. The keys are the exact
// strings that appear in column 0 of the "Supported formats" table (after
// stripping leading/trailing spaces and any markdown emphasis).
//
//nolint:gochecknoglobals // read-only lookup table; never mutated
var readmeFormatDisplayNames = map[string]format.FormatID{
"JPEG": format.FormatJPEG,
"TIFF": format.FormatTIFF,
"PNG": format.FormatPNG,
"WebP": format.FormatWebP,
"HEIF": format.FormatHEIF,
"AVIF": format.FormatAVIF,
"Canon CR2": format.FormatCR2,
"Canon CR3": format.FormatCR3,
"Nikon NEF": format.FormatNEF,
"Sony ARW": format.FormatARW,
"Adobe DNG": format.FormatDNG,
"Olympus ORF": format.FormatORF,
"Panasonic RW2": format.FormatRW2,
}
// stripFootnoteMarkers removes superscript footnote characters (¹²³⁴ and
// similar Unicode superscript digits/letters) from s. This lets "Yes²" and
// "Yes" compare equal.
func stripFootnoteMarkers(s string) string {
return strings.Map(func(r rune) rune {
// Unicode superscript digits and common footnote symbols.
switch r {
case '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹', '⁰',
'ⁱ', 'ⁿ', '†', '‡', '§', '¶':
return -1 // drop
}
return r
}, s)
}
// splitMarkdownRow splits a Markdown table row (| col | col | ...) into its
// cell values, trimmed of whitespace. The leading and trailing '|' delimiters
// are handled; empty cells are preserved.
func splitMarkdownRow(line string) []string {
line = strings.TrimSpace(line)
line = strings.Trim(line, "|")
parts := strings.Split(line, "|")
result := make([]string, len(parts))
for i, p := range parts {
result[i] = strings.TrimSpace(p)
}
return result
}
// isTableSeparator returns true for Markdown table separator rows
// (rows whose non-pipe characters are entirely dashes, colons, and spaces).
func isTableSeparator(line string) bool {
for _, r := range line {
if r != '|' && r != '-' && r != ':' && !unicode.IsSpace(r) {
return false
}
}
return strings.Contains(line, "-")
}
// TestREADMEWriteCapabilityMatchesCode is the doc↔code regression guard for
// task #193. It fails if:
// - The "Supported formats" table is missing from README.md.
// - A format whose Write column says "No" has SupportsWrite==true in code.
// - A format whose Write column says "Yes" (or "Yes²" etc.) has
// SupportsWrite==false in code.
//
// The test does NOT use t.Skip under any condition — if the table structure
// changes in a way that makes parsing impossible, that is itself a failure
// requiring a fix.
//
// This test must be kept in sync when a new format is added to the library.
// Regression history: divergence was introduced in Sprint 9 (commit 561f5d8)
// and remained undetected until task #193 (Sprint 33).
//
//nolint:paralleltest // uses os.ReadFile; no shared mutable state; serial is fine
func TestREADMEWriteCapabilityMatchesCode(t *testing.T) {
const readmePath = "README.md"
f, err := os.Open(readmePath)
if err != nil {
t.Fatalf("TestREADMEWriteCapabilityMatchesCode: cannot open %s: %v", readmePath, err)
}
defer func() {
if cerr := f.Close(); cerr != nil {
t.Errorf("TestREADMEWriteCapabilityMatchesCode: close %s: %v", readmePath, cerr)
}
}()
// ------------------------------------------------------------------ //
// Phase 1: locate and parse the "Supported formats" table.
// ------------------------------------------------------------------ //
//
// We scan for the header row that contains all five expected column
// titles. Once found, we consume rows until the table ends (a line that
// does not start with '|').
const tableAnchor = "| Format |"
type tableRow struct {
formatName string // column 0
writeValue string // column 3 (0-indexed)
}
var rows []tableRow
inTable := false
pastHeader := false
tableFound := false
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
trimmed := strings.TrimSpace(line)
if !inTable {
// Detect the table header row.
if strings.HasPrefix(trimmed, tableAnchor) {
inTable = true
tableFound = true
pastHeader = false
}
continue
}
// We are inside the table.
if !strings.HasPrefix(trimmed, "|") {
// Blank line or non-pipe line: table has ended.
break
}
if isTableSeparator(trimmed) {
// The separator row (|---|:---:|...) marks end of header.
pastHeader = true
continue
}
if !pastHeader {
// Still in the header area (multi-row header).
continue
}
// Data row.
cells := splitMarkdownRow(trimmed)
// We need at least 4 columns (Format, Extension(s), Read, Write).
if len(cells) < 4 {
continue
}
rows = append(rows, tableRow{
formatName: cells[0],
writeValue: stripFootnoteMarkers(cells[3]),
})
}
if err := scanner.Err(); err != nil {
t.Fatalf("TestREADMEWriteCapabilityMatchesCode: scanner error: %v", err)
}
if !tableFound {
// The table is structurally required; its absence is itself a failure.
t.Fatal("TestREADMEWriteCapabilityMatchesCode: \"Supported formats\" table not found in README.md — " +
"the table header row containing \"| Format |\" is required; " +
"if the table was renamed or removed, update this test AND the README together")
}
if len(rows) == 0 {
t.Fatal("TestREADMEWriteCapabilityMatchesCode: \"Supported formats\" table found but contains no data rows")
}
// ------------------------------------------------------------------ //
// Phase 2: correlate each table row with format.SupportsWrite.
// ------------------------------------------------------------------ //
for _, row := range rows {
fid, known := readmeFormatDisplayNames[row.formatName]
if !known {
// Unknown display name — either a new format was added without
// updating readmeFormatDisplayNames, or the table row is a
// footnote/legend line. Fail loudly so the test stays complete.
t.Errorf("README format %q is not in readmeFormatDisplayNames map — "+
"update readmeFormatDisplayNames in readme_write_capability_test.go "+
"when a new format is added", row.formatName)
continue
}
writeColUpper := strings.ToUpper(row.writeValue)
readmeClaimsWritable := strings.HasPrefix(writeColUpper, "YES")
readmeClaimsReadOnly := strings.HasPrefix(writeColUpper, "NO")
codeSupportsWrite := format.SupportsWrite(fid)
switch {
case readmeClaimsReadOnly && codeSupportsWrite:
// README says read-only but code disagrees — the core regression.
t.Errorf("README.md \"Supported formats\" table claims %q is read-only "+
"(Write=%q) but format.SupportsWrite(%v)==true in code; "+
"update the README Write column to match the code "+
"(regression: Sprint 9 divergence, task #193)",
row.formatName, row.writeValue, fid)
case readmeClaimsWritable && !codeSupportsWrite:
// README says writable but code disagrees — also a divergence.
t.Errorf("README.md \"Supported formats\" table claims %q is writable "+
"(Write=%q) but format.SupportsWrite(%v)==false in code; "+
"update the README Write column or the code to match",
row.formatName, row.writeValue, fid)
}
}
// ------------------------------------------------------------------ //
// Phase 3: verify every known writable FormatID has a table row.
// ------------------------------------------------------------------ //
//
// If a format is supported for write in code but is entirely absent from
// the README table (not just marked No), that is also a documentation gap.
seenNames := make(map[string]bool, len(rows))
for _, row := range rows {
seenNames[row.formatName] = true
}
for displayName, fid := range readmeFormatDisplayNames {
if format.SupportsWrite(fid) && !seenNames[displayName] {
t.Errorf("format %q (FormatID=%v) supports write in code but has no row "+
"in the README \"Supported formats\" table", displayName, fid)
}
}
}