Skip to content

Commit a150534

Browse files
dfa1claude
andcommitted
test(integration): encoding-table completeness fitness function
A second documentation fitness function (Neal Ford's sense: atomic/ triggered/static/automated), guarding COMPLETENESS where DocsConsistencyTest guards accuracy. The encodings table in docs/compatibility.md must list exactly the encodings the code registers via ServiceLoader — id, decoder/encoder class, and ✅/❌ flags all matched against the loaded EncodingDecoder/EncodingEncoder set. Notes stay human prose, unasserted. Asserts, never generates: on drift it prints the exact rows to add (missing) or the class/flag disagreements (wrong). Kill-tested both directions — removing a row and corrupting a class name each fail with the actionable message; the table currently passes clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent cd75397 commit a150534

2 files changed

Lines changed: 186 additions & 0 deletions

File tree

docs/testing.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,17 @@ run-once part and leave the hot, specialized part alone, with a comment saying w
182182
The throughline: let the tools point at the data, then decide with the context they do not
183183
have.
184184

185+
## Documentation fitness functions
186+
187+
Two [architectural fitness functions](https://www.thoughtworks.com/insights/books/building-evolutionary-architectures)
188+
(atomic / triggered / static / automated) guard the living docs, both in the integration module:
189+
190+
- `DocsConsistencyTest`**accuracy**: FQNs resolve, method claims name real methods,
191+
`META-INF/services` mentions exist, relative links don't dangle.
192+
- `EncodingTableFitnessTest`**completeness**: the encodings table in `docs/compatibility.md`
193+
lists exactly the encodings the code registers via `ServiceLoader` (id, decoder/encoder class,
194+
✅/❌ flags), asserted not generated — on drift it prints the exact rows to add.
195+
185196
## Docs consistency gate
186197

187198
`DocsConsistencyTest` (integration module, plain surefire) machine-checks the living docs
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package io.github.dfa1.vortex.integration;
2+
3+
import io.github.dfa1.vortex.reader.decode.EncodingDecoder;
4+
import io.github.dfa1.vortex.writer.encode.EncodingEncoder;
5+
import org.junit.jupiter.api.BeforeAll;
6+
import org.junit.jupiter.api.Test;
7+
8+
import java.io.IOException;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
import java.util.ArrayList;
12+
import java.util.LinkedHashMap;
13+
import java.util.List;
14+
import java.util.Map;
15+
import java.util.ServiceLoader;
16+
import java.util.TreeMap;
17+
import java.util.regex.Matcher;
18+
import java.util.regex.Pattern;
19+
20+
import static org.assertj.core.api.Assertions.assertThat;
21+
22+
/// Architectural fitness function (Neal Ford, *Building Evolutionary Architectures*) guarding a
23+
/// COMPLETENESS characteristic of the documentation: the encodings support table in
24+
/// `docs/compatibility.md` must list exactly the encodings the code actually registers.
25+
///
26+
/// Atomic / triggered / static / automated. Complements [DocsConsistencyTest], which guards
27+
/// *accuracy* (no stale claim); this guards *completeness* (no missing row) — add an encoding
28+
/// without documenting it and the build goes red.
29+
///
30+
/// Ground truth is the `ServiceLoader` set, not a hand-list: every service-registered
31+
/// [EncodingDecoder]/[EncodingEncoder] contributes its `encodingId()` wire string, its decode and
32+
/// encode support, and its implementing class's simple name. The table must match on all four —
33+
/// id present, decoder class named, encoder class named, ✅/❌ flags correct. It asserts, it never
34+
/// rewrites the doc; on drift the message prints the exact rows to add.
35+
///
36+
/// The `Notes` column is human prose (per-encoding supported dtypes) and is deliberately NOT
37+
/// asserted — a fitness function checks the objective characteristic, not editorial content.
38+
class EncodingTableFitnessTest {
39+
40+
/// One table row's machine-checkable content (Notes excluded).
41+
private record Row(String decoderClass, String encoderClass, boolean decode, boolean encode) {
42+
}
43+
44+
private static final Pattern TABLE_ROW = Pattern.compile(
45+
"^\\|\\s*`([a-z][a-z0-9._]*)`\\s*\\|\\s*`?([A-Za-z0-9]*)`?\\s*\\|\\s*`?([A-Za-z0-9]*)`?\\s*"
46+
+ "\\|\\s*([✅❌])\\s*\\|\\s*([✅❌])\\s*\\|",
47+
Pattern.MULTILINE);
48+
49+
private static Map<String, Row> documented;
50+
private static Map<String, String> decoderClassById;
51+
private static Map<String, String> encoderClassById;
52+
53+
@BeforeAll
54+
static void loadGroundTruthAndTable() throws IOException {
55+
decoderClassById = new TreeMap<>();
56+
for (EncodingDecoder decoder : ServiceLoader.load(EncodingDecoder.class)) {
57+
decoderClassById.put(decoder.encodingId().id(), decoder.getClass().getSimpleName());
58+
}
59+
encoderClassById = new TreeMap<>();
60+
for (EncodingEncoder encoder : ServiceLoader.load(EncodingEncoder.class)) {
61+
encoderClassById.put(encoder.encodingId().id(), encoder.getClass().getSimpleName());
62+
}
63+
64+
Path compat = repoRoot().resolve("docs/compatibility.md");
65+
String encodingsSection = sliceEncodingsTable(Files.readString(compat));
66+
documented = new LinkedHashMap<>();
67+
Matcher m = TABLE_ROW.matcher(encodingsSection);
68+
while (m.find()) {
69+
documented.put(m.group(1), new Row(
70+
m.group(2), m.group(3),
71+
m.group(4).equals("✅"), m.group(5).equals("✅")));
72+
}
73+
}
74+
75+
@Test
76+
void everyRegisteredEncoding_hasATableRow() {
77+
// Given every encoding id the code registers a decoder or encoder for
78+
var allIds = new TreeMap<String, Row>();
79+
for (String id : union(decoderClassById.keySet(), encoderClassById.keySet())) {
80+
allIds.put(id, new Row(
81+
decoderClassById.getOrDefault(id, ""),
82+
encoderClassById.getOrDefault(id, ""),
83+
decoderClassById.containsKey(id),
84+
encoderClassById.containsKey(id)));
85+
}
86+
87+
// When missing rows are collected, rendered as ready-to-paste markdown
88+
var missing = new ArrayList<String>();
89+
allIds.forEach((id, expected) -> {
90+
if (!documented.containsKey(id)) {
91+
missing.add(renderRow(id, expected));
92+
}
93+
});
94+
95+
// Then the table documents every registered encoding
96+
assertThat(missing).as("encodings registered in code but MISSING from the "
97+
+ "docs/compatibility.md table — add these rows (Notes are yours to fill):\n"
98+
+ String.join("\n", missing)).isEmpty();
99+
}
100+
101+
@Test
102+
void everyTableRow_matchesARegisteredEncoding() {
103+
// Given every row in the documented table
104+
var wrong = new ArrayList<String>();
105+
106+
// When each is checked against the ServiceLoader ground truth
107+
documented.forEach((id, row) -> {
108+
boolean known = decoderClassById.containsKey(id) || encoderClassById.containsKey(id);
109+
if (!known) {
110+
wrong.add(id + ": no registered decoder or encoder (phantom row)");
111+
return;
112+
}
113+
if (row.decode() != decoderClassById.containsKey(id)) {
114+
wrong.add(id + ": Decode flag says " + tick(row.decode())
115+
+ " but a decoder is " + (decoderClassById.containsKey(id) ? "" : "NOT ") + "registered");
116+
}
117+
if (row.encode() != encoderClassById.containsKey(id)) {
118+
wrong.add(id + ": Encode flag says " + tick(row.encode())
119+
+ " but an encoder is " + (encoderClassById.containsKey(id) ? "" : "NOT ") + "registered");
120+
}
121+
if (row.decode() && !row.decoderClass().equals(decoderClassById.get(id))) {
122+
wrong.add(id + ": Decoder column says `" + row.decoderClass()
123+
+ "` but the registered class is `" + decoderClassById.get(id) + "`");
124+
}
125+
if (row.encode() && !row.encoderClass().equals(encoderClassById.get(id))) {
126+
wrong.add(id + ": Encoder column says `" + row.encoderClass()
127+
+ "` but the registered class is `" + encoderClassById.get(id) + "`");
128+
}
129+
});
130+
131+
// Then no row is a phantom, and every class name and flag matches reality
132+
assertThat(wrong).as("rows in the docs/compatibility.md encodings table that disagree "
133+
+ "with the registered encodings:\n" + String.join("\n", wrong)).isEmpty();
134+
}
135+
136+
// ── helpers ───────────────────────────────────────────────────────────────
137+
138+
private static String sliceEncodingsTable(String doc) {
139+
int start = doc.indexOf("## Encodings");
140+
assertThat(start).as("## Encodings section in compatibility.md").isNotNegative();
141+
int end = doc.indexOf("\n## ", start + 1);
142+
return end < 0 ? doc.substring(start) : doc.substring(start, end);
143+
}
144+
145+
private static String renderRow(String id, Row r) {
146+
return String.format("| `%s` | %s | %s | %s | %s | |",
147+
id,
148+
r.decoderClass().isEmpty() ? "—" : "`" + r.decoderClass() + "`",
149+
r.encoderClass().isEmpty() ? "—" : "`" + r.encoderClass() + "`",
150+
tick(r.decode()), tick(r.encode()));
151+
}
152+
153+
private static String tick(boolean b) {
154+
return b ? "✅" : "❌";
155+
}
156+
157+
private static <T> List<T> union(java.util.Set<T> a, java.util.Set<T> b) {
158+
var out = new ArrayList<>(a);
159+
b.forEach(x -> {
160+
if (!out.contains(x)) {
161+
out.add(x);
162+
}
163+
});
164+
return out;
165+
}
166+
167+
private static Path repoRoot() {
168+
Path dir = Path.of("").toAbsolutePath();
169+
while (dir != null && !Files.exists(dir.resolve("CLAUDE.md"))) {
170+
dir = dir.getParent();
171+
}
172+
assertThat(dir).as("repo root (directory with CLAUDE.md)").isNotNull();
173+
return dir;
174+
}
175+
}

0 commit comments

Comments
 (0)