Files
json/docs/mkdocs/scripts/figures/qa_pipeline.py
T
Niels Lohmann 444dba381f docs: add figures, table captions and a macro chooser
- Figures with numbered captions: SAX stepper, json_view layout and edit
  stepper, exception map, JSON pointer and patch walkers, QA pipeline,
  scaling and binary size charts, roadmap lanes, release timeline,
  integration decision tree and namespace builder
- Table captions (nj-table-caption) on 9 pages
- Compile times: modules and PCH charts, filesystem section, re-measured
  Figure 1, links between features/modules.md and compile_times.md
- Interactive macro chooser for features/arbitrary_types.md
- Link check: exclude URLs that answer the plugin with 400/401/419 or are
  dead (customers.md)

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 10:42:00 +01:00

197 lines
7.7 KiB
Python

"""Draws the quality-assurance pipeline overview of community/quality_assurance.md as an inline SVG into
docs/images/qa_pipeline.svg.
The SVG is not an image: the page includes its text with `--8<-- "images/qa_pipeline.svg"`, so it picks up the colours
and fonts of the page (see the `nj-fig` rules in docs/css/brand.css and the `nj-exceptions-*` rules in
docs/css/fig-exceptions.css). The file holds two drawings, a three-column one and a one-column one for phones; the CSS
shows one of them.
The lanes, tools and counts are taken from the text of community/quality_assurance.md, the trigger badges from the
`on:` sections of .github/workflows/*.yml. Update LANES when the page changes, run
`python3 scripts/figures/qa_pipeline.py` from docs/mkdocs, and commit the generated file. In SVG text write the word
JSON as `J&#8203;SON`, otherwise the abbreviation extension wraps it in an <abbr> and the text disappears.
"""
import html
import re
from pathlib import Path
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
P = "nj-exceptions-"
# trigger badges: key -> (label, css modifier)
TRIGGERS = {
"push": ("every push / PR", "push"),
"pr": ("every PR", "push"),
"sched": ("scheduled", "sched"),
"cont": ("continuous", "cont"),
}
# title, triggers, lines (at most 31 characters, so they fit a card of the three-column drawing)
LANES = [
("Build matrix", ["push"], [
"50+ compilers: GCC, Clang, MSVC,",
"AppleClang, Intel, NVIDIA, ...",
"C++11, 14, 17, 20, 23, and 26",
"libc++ and libstdc++",
"warnings: Clang -Weverything,",
"GCC 300+ warnings",
]),
("Unit tests (doctest)", ["push"], [
"every public API function",
"every exception id and message",
"all Unicode code points",
"single header and split headers",
"custom number, string, array,",
"and object types",
]),
("Coverage", ["push"], [
"unit tests cover all lines",
"of the code base",
]),
("Static analysis", ["push", "sched"], [
"Clang-Tidy, Cppcheck, Infer",
"Clang Static Analyzer (52",
"checkers), IWYU, Codacy",
"CodeQL, Semgrep, Flawfinder",
"(also scheduled)",
]),
("Dynamic analysis", ["push"], [
"runtime assertions",
"Valgrind (Memcheck)",
"address and undefined behavior",
"sanitizers, integer overflow,",
"nullability violations",
]),
("Fuzzing", ["pr", "cont"], [
"CIFuzz on every pull request",
"OSS-Fuzz: billions of inputs",
"every crash fixed with a",
"reproducing unit test",
]),
("Style", ["push"], [
"Artistic Style (astyle),",
"enforced in the CI",
"cpplint with 61 rules",
]),
("Docs and examples", ["push", "sched"], [
"every documentation example is",
"tested; changed output fails",
"site built with warnings",
"as errors; links checked",
"regularly (scheduled)",
]),
("Integration, supply chain", ["push", "sched"], [
"CMake 3.5, 3.31.6, and 4.0.0",
"Meson, Swift Package Manager",
"dependency review on every PR",
"Dependabot daily, OpenSSF",
"Scorecard, REUSE",
]),
]
GLOSSARY = Path(__file__).resolve().parents[2] / "includes" / "glossary.md"
ABBRS = sorted(re.findall(r"^\*\[([^\]]+)\]:", GLOSSARY.read_text(), re.M), key=len, reverse=True)
def noabbr(s):
"""Breaks every glossary abbreviation with a zero-width space, so the abbr extension leaves SVG text alone."""
for a in ABBRS:
s = re.sub(r"(?<![\w&#])%s(?!\w)" % re.escape(a), a[0] + "&#8203;" + a[1:], s)
return s
def esc(s):
return html.escape(s, quote=True)
def txt(s):
return noabbr(esc(s))
def badge_w(label):
return len(label) * 6.3 + 16
def card(x, y, w, h, title, triggers, lines):
s = ['<g class="%scard">' % P,
'<rect class="%sbox" x="%g" y="%g" width="%g" height="%g" rx="8"/>' % (P, x, y, w, h),
'<text class="%sctitle" x="%g" y="%g">%s</text>' % (P, x + 14, y + 25, txt(title))]
bx = x + 14
for t in triggers:
label, mod = TRIGGERS[t]
bw = badge_w(label)
s.append('<rect class="%sbadge %sbadge--%s" x="%g" y="%g" width="%g" height="20" rx="10"/>' % (P, P, mod, bx, y + 36, bw))
s.append('<text class="%sbadgetext %sbadgetext--%s" x="%g" y="%g">%s</text>' % (P, P, mod, bx + bw / 2, y + 50, txt(label)))
bx += bw + 6
for i, line in enumerate(lines):
s.append('<text class="%sline" x="%g" y="%g">%s</text>' % (P, x + 14, y + 78 + i * 18, txt(line)))
s.append('</g>')
return "\n".join(s)
def pill(cx, y, w, label, sub=None, cls=""):
h = 46 if sub else 34
s = ['<rect class="%sstage%s" x="%g" y="%g" width="%g" height="%d" rx="%g"/>' % (P, cls, cx - w / 2, y, w, h, h / 2),
'<text class="%sstagetext" x="%g" y="%g">%s</text>' % (P, cx, y + 22, txt(label))]
if sub:
s.append('<text class="%sstagesub" x="%g" y="%g">%s</text>' % (P, cx, y + 38, txt(sub)))
return "\n".join(s), h
def arrow(x, y1, y2, mid):
return '<line class="%sedge" x1="%g" y1="%g" x2="%g" y2="%g" marker-end="url(#%s)"/>' % (P, x, y1, x, y2, mid)
def drawing(W, cols, suffix):
mid = "nj-qa-arrow-" + suffix
body = []
cx = W / 2
s, h = pill(cx, 8, 230, "push or pull request")
body.append(s)
y = 8 + h
body.append(arrow(cx, y, y + 26, mid))
# container with the lanes
top = y + 28
pad, gap = 14, 12
cw = (W - 24 - 2 * pad - (cols - 1) * gap) / cols
body.append('<text class="%sgroup" x="%g" y="%g">checks, run in parallel on GitHub Actions and AppVeyor</text>' % (P, 12 + pad, top + 24) if cols > 1 else
'<text class="%sgroup" x="%g" y="%g">checks, run in parallel</text>' % (P, 12 + pad, top + 24))
gy = top + 40
rows = [LANES[i:i + cols] for i in range(0, len(LANES), cols)]
for r in rows:
ch = 78 + 18 * (max(len(l[2]) for l in r) - 1) + 16
for j, (title, trig, lines) in enumerate(r):
body.append(card(12 + pad + j * (cw + gap), gy, cw, ch, title, trig, lines))
gy += ch + gap
bottom = gy - gap + pad
body.insert(0, '<rect class="%sgroupbox" x="12" y="%g" width="%g" height="%g" rx="12"/>' % (P, top, W - 24, bottom - top))
# after the checks
body.append(arrow(cx, bottom, bottom + 26, mid))
s, h = pill(cx, bottom + 28, 230, "merge into develop", cls=" %sstage--merge" % P)
body.append(s)
y = bottom + 28 + h
body.append(arrow(cx, y, y + 26, mid))
s, h = pill(cx, y + 28, 300, "release", "no open vulnerability alert", cls=" %sstage--merge" % P)
body.append(s)
H = y + 28 + h + 8
defs = ('<defs><marker id="%s" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">'
'<path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs>' % mid)
tid, did = "nj-qa-title-" + suffix, "nj-qa-desc-" + suffix
desc = ("Every push or pull request starts these checks in parallel: " +
"; ".join("%s (%s): %s" % (t, ", ".join(TRIGGERS[k][0] for k in trig), " ".join(lines)) for t, trig, lines in LANES) +
". When they pass, the change is merged into develop; a release is made only when no vulnerability alert is open.")
out = ['<svg class="nj-fig %sqa %sqa--%s" viewBox="0 0 %d %d" role="img" aria-labelledby="%s %s" xmlns="http://www.w3.org/2000/svg">'
% (P, P, "narrow" if cols == 1 else "wide", W, H, tid, did),
'<title id="%s">Quality assurance pipeline</title>' % tid,
'<desc id="%s">%s</desc>' % (did, txt(desc)),
defs] + body + ['</svg>']
return "\n".join(out)
def main():
(OUT / "qa_pipeline.svg").write_text(drawing(760, 3, "w") + "\n" + drawing(360, 1, "n") + "\n")
main()