- 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>
197 lines
7.7 KiB
Python
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​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] + "​" + 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()
|