One LaTeX design for a company, in three templates

A company that writes with LaTeX ends up with a document class, a letter class
and a presentation theme that share a design and not a line of code. Every fix
is then made three times, two of them late and the third never.

Here the page is written once. A company declares its colours, faces and logo in
one file, and the document template, the letter class and the presentation theme
read that one file. Nothing in the suite carries a colour value, a font name or
a file name of any company, which is what lets it be published while the
companies stay private.

Every measure of the page follows from a measurement or from a named definition:
the room the head and the foot need is taken from the boxes they really build,
one line of the body text stands between the head and the text and between the
text and the foot, a heading never stands alone at the foot of a page, a picture
takes the size of the family, and a Markdown file reaches the same page as the
same document written in LaTeX.

The suite ships with a company that does not exist, Nordwind AG, so that it
builds and is measured anywhere: three colours, the TeX Gyre families every
installation carries, and an icon drawn in TikZ.

574 checks over five measurements, all of them on the rendered page or on the
build log rather than on the source.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc WäckerlinandClaude Opus 5 committed 2026-09-22 15:06:07 +02:00
commit adc58e19d7
39 files changed
+6627

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
\documentclass[a4paper,10pt]{article}
\usepackage[plain]{nordwind}
\begin{document}
Lorem ipsum dolor sit amet, consetetur sadipscing elitr. \logo{}
\end{document}
+528
View File
@@ -0,0 +1,528 @@
#!/usr/bin/env python3
"""The identity interface: the suite carries no company, and the company reaches it.
Two kinds of question, and both are needed. What the suite may NOT contain is a
property of its source, so it is measured there. What a company really reaches on
the page is a property of the page, so it is measured there.
"""
import os
import re
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import support
from support import Check, Page, build
EXAMPLES = os.path.join(support.ROOT, "examples")
SUITE_FILES = ("business-identity.sty", "business-suite.sty",
"business-brief.cls", "business-beamer.sty")
# A colour value in the suite is a company's property in a file every other company
# loads. The palette belongs to the identity of the company, the roles to the
# suite, and a role carries a name and never a number.
COLOUR_VALUE = re.compile(r"\\definecolor\s*\{[^}]*\}\s*\{(HTML|RGB|rgb|cmyk|gray)\}")
# The three templates take the identity from one place and carry none of their
# own: not a colour, not a face. A copy passes every check that asks whether
# the values are right, and is wrong on the day the one place changes.
TEMPLATES = ("business-suite.sty", "business-brief.cls", "business-beamer.sty")
OWN_VALUE = re.compile(r"^\s*(\\definecolor|\\colorlet|\\setmainfont"
r"|\\setsansfont|\\setmonofont|\\newfontfamily"
r"|\\renewcommand\*?\s*\\familydefault)", re.MULTILINE)
# Everything after \endinput is dead: LaTeX stops reading there. A block moved
# to the end of a file lands behind it without a word in any log, and every
# document is then built without what it does. Measured in a sister package on
# 2026-09-21, where the head height was measured by code that never ran.
ENDINPUT = re.compile(r"^\\endinput\s*$", re.MULTILINE)
DOCUMENT = r"""\documentclass[a4paper,10pt]{article}
\usepackage{nordwind}
\title{Lorem Ipsum}
\author{Erika Muster}
\date{21 September 2026}
\begin{document}
\maketitle
\section{Consetetur}
Dolor sit amet \success{} and \fail{} and an arrow →.
\end{document}
"""
# A company whose logo is a FILE and not a drawing, which is the other of the two
# ways the suite carries a logo and the one the README shows. `example-image` is
# the test image of TeX Live and lies on every installation.
FILE_COMPANY = r"""\ProvidesPackage{filecompany}[2026/09/21 A company with a logo file]
\RequirePackage{business-identity}
\businesscolours{brand = blue}
\setmainfont{Latin Modern Roman}
\businessfonts{default = rm, smallcaps = false}
\businessidentity{
name = Filecompany,
url = https://example.org,
address = Filecompany — https://example.org,
lockup = icon+text,
icon = example-image,
}
\DeclareOption*{\PassOptionsToPackage{\CurrentOption}{business-suite}}
\ProcessOptions\relax
\RequirePackage{business-suite}
\endinput
"""
FILE_DOCUMENT = r"""\documentclass[a4paper,10pt]{article}
\usepackage[nologo]{filecompany}
\begin{document}
\pagestyle{empty}
\noindent\logo[height=1cm]
\noindent\logo[height=2cm]
\end{document}
"""
LANGUAGE = r"""\documentclass[a4paper,10pt,ngerman]{article}
\usepackage{nordwind}
\usepackage[ngerman]{babel}
\title{Lorem Ipsum}
\author{Erika Muster}
\begin{document}
\maketitle
Dolor sit amet.
\end{document}
"""
# A company of two lines: the name, and the sentence it sets under the name, in
# a face and a colour of its own. Two pages, because the first carries the
# address line under the block and every other does not, and a logo in a
# sentence, because that one stays the one line it has always been.
TAGLINE_DOCUMENT = r"""\documentclass[a4paper,10pt]{article}
\usepackage{nordwind}
\businessidentity{tagline={Vernunft und Freiheit},
name face={\bfseries},
%s}
\title{Lorem Ipsum}
\author{Erika Muster}
\begin{document}
\maketitle
\section{Consetetur}
Lorem ipsum dolor sit amet. The logo in a sentence: \logo{} stays one line.
\newpage
\section{Dolor Sit}
At vero eos et accusam.
\end{document}
"""
TAGLINE_QUIET = TAGLINE_DOCUMENT % "tagline color=business-running-quiet"
# The same company without a word about the colour of its second line. Nothing
# is then set for that line, so it stands in the colour of the name over it.
TAGLINE_PLAIN = TAGLINE_DOCUMENT % "url=https://nordwind.example"
# The lockup alone on a page, so nothing but it carries ink. `nologo` keeps the
# head empty; what stands there is the one call being measured.
SIZES = r"""\documentclass[a4paper,10pt]{article}
\usepackage[nologo]{nordwind}
\businessidentity{tagline={Vernunft und Freiheit}}
\begin{document}
\thispagestyle{empty}
\noindent\logo[tagline, height=2cm%s]
\end{document}
"""
SIZES_MEASURED = SIZES % ""
SIZES_FACTOR = SIZES % ", taglinefactor=0.4"
SIZES_HIGH = SIZES % ", iconheight=3cm"
SIZES_WIDE = SIZES % ", iconwidth=1cm"
def spread(found, *words):
"""The left and the right edge of a group of words of one line."""
boxes = [word for word in found if word[0] in words]
if not boxes:
return None
return min(box[1] for box in boxes), max(box[3] for box in boxes)
def icon_box(page):
"""The rectangle the icon stands in: the leftmost group of ink columns."""
columns = page.ink_columns()
if not columns:
return None
end = columns[0]
for column in columns[1:]:
if column - end > 2:
break
end = column
rows = [y for y in range(page.height)
if any(page.dark(x, y) for x in range(columns[0], end + 1))]
factor = 72.0 / page.dpi
return ((end - columns[0] + 1) * factor, (rows[-1] - rows[0] + 1) * factor)
def lockup_sizes(check):
"""The two sizes of the lockup that measure themselves.
The tagline is set to the size at which it is exactly as WIDE as the name
over it, so the block has one left edge and one right edge. And the icon is
as HIGH as that block, because the mark stands beside the text and a height
of its own agrees with the text at one size of type and at no other. Both
are measured here, and both overrides with them: a factor for the tagline
and a height or a width for the icon, each one taking the measurement out of
the way for that call alone.
"""
source = support.write("lockupsizes.tex", SIZES_MEASURED)
pdf, _ = build(source, EXAMPLES)
found = support.words(pdf, 1)
name = spread(found, "Nordwind")
tag = spread(found, "Vernunft", "und", "Freiheit")
check.that(name and tag, "the lockup did not reach the page")
if not name or not tag:
return
wide, under = name[1] - name[0], tag[1] - tag[0]
check.that(abs(wide - under) < 1,
f"the name is {wide:.1f} points wide and the tagline "
f"{under:.1f}: the second line is set to the width of the first")
check.that(abs(name[0] - tag[0]) < 1,
"the two lines of the block do not share their left edge")
# The icon spans the block: from the top of the first line to the bottom of
# the last. Measured at the ink of the drawing, which fills its own box.
page = Page(pdf, 1)
box = icon_box(page)
check.that(box, "the icon did not reach the page")
if not box:
return
block = max(word[4] for word in found) - min(word[2] for word in found)
check.that(abs(box[1] - block) < 4,
f"the icon is {box[1]:.1f} points high and the text beside it "
f"{block:.1f}: the icon follows the text")
# And each override takes one of the two measurements out of the way.
source = support.write("lockupfactor.tex", SIZES_FACTOR)
pdf, _ = build(source, EXAMPLES)
found = support.words(pdf, 1)
name = spread(found, "Nordwind")
tag = spread(found, "Vernunft", "und", "Freiheit")
if name and tag:
check.that(tag[1] - tag[0] < name[1] - name[0] - 5,
"a company that names a factor for its tagline still got "
"the line set to the width of the name")
for name, fixture, index, asked in (("high", SIZES_HIGH, 1, 85.04),
("wide", SIZES_WIDE, 0, 28.35)):
source = support.write("lockup%s.tex" % name, fixture)
pdf, _ = build(source, EXAMPLES)
box = icon_box(Page(pdf, 1))
check.that(box, f"the icon of the {name} fixture did not reach the page")
if not box:
continue
# Three centimetres are 85.04 points and one is 28.35. The drawing fills
# its box, so a point and a half is the rasterizer.
check.that(abs(box[index] - asked) < 2,
f"the icon was asked for at {asked:.1f} points and came out "
f"at {box[index]:.1f}")
# The width follows the height in the proportions of the drawing: the icon
# of this company is a disc, so the two are the same number.
source = support.write("lockuphigh.tex", SIZES_HIGH)
pdf, _ = build(source, EXAMPLES)
box = icon_box(Page(pdf, 1))
if box:
check.that(abs(box[0] - box[1]) < 2,
f"the icon came out {box[0]:.1f} wide and {box[1]:.1f} high, "
"so the proportions of the drawing were not kept")
def source_checks(check):
for name in SUITE_FILES:
path = os.path.join(support.SUITE, name)
with open(path, encoding="utf-8") as handle:
text = handle.read()
found = COLOUR_VALUE.search(text)
check.that(found is None,
f"{name} carries a colour value: {found.group(0) if found else ''}")
ends = ENDINPUT.search(text)
if ends:
rest = text[ends.end():].strip()
check.that(rest == "",
f"{name} carries {len(rest.splitlines())} lines after "
"\\endinput, which LaTeX never reads")
if name in TEMPLATES:
own = OWN_VALUE.search(text)
check.that(own is None,
f"{name} sets a colour or a face of its own: "
f"{own.group(1) if own else ''} — it has to take both "
"from the identity it loads")
# Every role a template reads has to be declared, or the build fails with
# "undefined color" in a company that does not set it.
declared = set()
with open(os.path.join(support.SUITE, "business-identity.sty"),
encoding="utf-8") as handle:
identity = handle.read()
for name in re.findall(r"\\business@role\{([a-z-]+)\}", identity):
declared.add(name)
used = set()
for name in SUITE_FILES:
with open(os.path.join(support.SUITE, name), encoding="utf-8") as handle:
used |= set(re.findall(r"business-([a-z-]+)", handle.read()))
# The names of the packages themselves are no roles.
used -= {"identity", "suite", "brief", "beamer"}
missing = sorted(used - declared)
check.that(not missing, f"roles read but never declared: {missing}")
def page_checks(check):
source = support.write("identity-probe.tex", DOCUMENT)
pdf, _ = build(source, os.path.join(support.ROOT, "examples"))
page = Page(pdf, 1)
# The logo stands at the right edge of the type area, on the same edge as
# the text under it. Everything above the title belongs to the head.
whole = page.box()
check.that(whole is not None, "the page carries no ink at all")
left, top, right, bottom = whole
head = page.box(0, int(0.12 * page.height))
check.that(head is not None, "the head of the page is empty")
check.that(abs(head[2] - right) < 6,
f"the head does not end on the right edge of the text: "
f"{head[2]:.1f} against {right:.1f}")
# The page number stands at the foot, at the outer edge. Which rows the
# foot occupies is READ off the page and not guessed from a share of its
# height: the head and the foot follow the face of the company, and a share
# that holds for one company misses the foot of the next — measured when the
# example company changed its faces and the band that was searched came back
# empty.
rows = page.ink_rows()
last, current = [], []
for row in rows:
if current and row - current[-1] > 2:
last = current
current = []
current.append(row)
foot = page.box(current[0], current[-1] + 1) if current else None
check.that(foot is not None, "the foot carries no page number")
check.that(foot is not None and abs(foot[2] - right) < 6,
f"the page number is not at the right edge: "
f"{foot[2] if foot else 0:.1f} against {right:.1f}")
# What the PDF says about itself. A file whose properties say "untitled" is
# a file nobody finds again.
#
# It is read out of the XMP stream and not out of the info dictionary: that
# is where a file search, a reading program and an archive look today, and
# `hyperxmp` writes the author THERE and nowhere else — measured here, where
# `pdfinfo` prints a title and no author for a document that carries both.
xmp = support.run(["pdfinfo", "-meta", pdf]).stdout
check.that("<dc:title>" in xmp and "Lorem Ipsum" in xmp,
"the PDF carries no title")
check.that("<dc:creator>" in xmp and "Erika Muster" in xmp,
"the PDF carries no author")
# And the language, where the document says one. The template sets it
# nowhere: hyperref takes it from babel, and a second setting would only
# warn. That this really happens is measured and not assumed.
spoken = support.write("language-probe.tex", LANGUAGE)
speaking, _ = build(spoken, os.path.join(support.ROOT, "examples"))
said = support.run(["pdfinfo", "-meta", speaking]).stdout
check.that("dc:language" in said and "de" in said,
"a document that declares its language carries none in its XMP")
# The faces of the company, and nothing that LaTeX fell back to. Computer
# Modern is what arrives when a font has no shape in the encoding in force.
faces = support.fonts(pdf)
check.that(faces, "the PDF embeds no font at all")
fallbacks = [face for face in faces if face.startswith("CMR")
or face.startswith("CMSS") or face.startswith("SFRM")]
check.that(not fallbacks, f"LaTeX fell back to {fallbacks}")
# The marks come from the symbol family, because no text face carries them.
check.that(any("DejaVu" in face for face in faces),
f"the marks did not reach the page: {faces}")
def plain(check):
"""`plain` gives the identity and never the page of a document.
The two templates that ask for it are the letter class and the presentation
theme, and neither of them is an ordinary document: a page style and a
geometry laid over them wrecks their own layout. The case is measured with
the smallest document that can show it, because the failure is not a wrong
page — it is a run that stops: TeX skips a branch by counting `\\if` and
`\\fi` TOKENS, so a flag declared inside the skipped block closes the skip
with its own `\\fi` and the whole block runs. Reported on 2026-09-21 from a
fourth company, where the letter and the deck stopped building.
"""
source = os.path.join(support.ROOT, "tests", "documents", "plain.tex")
pdf, log = build(source, os.path.join(support.ROOT, "examples"))
check.that(os.path.exists(pdf), "a document with `plain` does not build")
with open(log, encoding="utf-8", errors="replace") as handle:
text = handle.read()
# Nothing of the page: no head, no foot, no geometry.
check.that("fancyhdr" not in text,
"`plain` loads fancyhdr, so it carries the page of a document")
check.that("geometry.sty" not in text,
"`plain` loads geometry, so it lays out the page again")
check.that("longtable" not in text,
"`plain` loads the table machinery of the document template")
def logo_from_file(check):
"""The other way a company carries its logo: a file instead of a drawing.
Two heights in one document, and the second asks for twice the first. A
height arrives as whatever the document writes — `1cm`, `2em`, `40pt` — and
a computation that reads the number and drops the unit passes at one of
them and fails at the other: measured in a sister package, where the gap
under the logo collapsed at every height given in centimetres while the
same formula looked right in em.
"""
support.write("filecompany.sty", FILE_COMPANY)
source = support.write("logofile.tex", FILE_DOCUMENT)
pdf, _ = build(source, support.BUILD)
page = Page(pdf, 1)
rows = page.ink_rows()
check.that(rows, "the logo file did not reach the page at all")
if not rows:
return
blocks, current = [], []
for row in rows:
if current and row - current[-1] > 2:
blocks.append(current)
current = []
current.append(row)
blocks.append(current)
check.that(len(blocks) == 2,
f"the page should carry two logos, it carries {len(blocks)}")
if len(blocks) != 2:
return
small = blocks[0][-1] - blocks[0][0] + 1
large = blocks[1][-1] - blocks[1][0] + 1
ratio = large / small
check.that(1.9 < ratio < 2.1,
f"the logo asked for at twice the height came out {ratio:.2f} "
"times as high")
def tagline(check):
"""The lockup of two lines: the name, and the company's sentence under it.
Four things are measured, and each of them is a decision the family took.
The head of the first page carries three lines — name, tagline, address —
and the head of the second two, because the address belongs on the sheet
that leaves the company and the tagline belongs to the mark. The logo in a
SENTENCE stays one line, because a place asks for the tagline and a company
only says what it is; without that division a mark in running text would
open the line it stands in. And the page reserves the room for the taller
block: the head is measured when the identity arrives, and a template that
measured it before the company declared itself asks fancyhdr for the
difference on every page — 16.8 points, measured on 2026-09-22.
"""
source = support.write("taglinedoc.tex", TAGLINE_QUIET)
pdf, log = build(source, EXAMPLES)
with open(log, encoding="utf-8") as handle:
printed = handle.read()
check.that("headheight is too small" not in printed,
"the page kept less room for the head than the two-line lockup "
"takes, so fancyhdr asked for the difference")
for number, carries in ((1, True), (2, False)):
found = support.words(pdf, number)
head = [word for word in found if word[4] < 90]
spoken = " ".join(word[0] for word in head)
check.that("Vernunft" in spoken,
f"the tagline is not in the head of page {number}: "
f"{spoken}")
check.that(("nordwind.example" in spoken) == carries,
f"the address line is {'missing from' if carries else 'in'} "
f"the head of page {number}, and it belongs on the first "
"page alone")
name = [word for word in head if word[0] == "Nordwind"]
tag = [word for word in head if word[0] == "Vernunft"]
if not name or not tag:
continue
name, tag = name[0], tag[0]
# The two lines stand flush left with each other, and the second is the
# smaller: the family sets the tagline at a share of the name.
check.that(abs(name[1] - tag[1]) < 1,
f"the name begins at {name[1]:.1f} points and the tagline "
f"at {tag[1]:.1f}: the two lines of the block share their "
"left edge")
check.that(tag[2] > name[4] - 1,
"the tagline does not stand under the name")
high, low = name[4] - name[2], tag[4] - tag[2]
check.that(low < high,
f"the tagline is {low:.1f} points high and the name "
f"{high:.1f}: the second line of the lockup is the smaller "
"one")
# A logo in a SENTENCE carries no tagline: the name stands in the line of
# the body text, and the tagline of the company is nowhere near it.
body = [word for word in support.words(pdf, 1) if word[2] > 200]
inline = [word for word in body if word[0] == "Nordwind"]
check.that(inline, "the logo in the sentence did not reach the page")
if inline:
near = [word for word in body
if word[0] == "Vernunft"
and abs(word[2] - inline[0][2]) < 30]
check.that(not near,
"the logo in a sentence carries the tagline, so a mark in "
"running text opens the line it stands in")
def darkest(page, word):
"""The darkest pixel inside the box a word stands in, 0 to 255.
The darkest pixel is the core of a stem, and that is the colour the type is
set in: the mean over a word measures how much white stands between its
letters, which is a property of the letters and of the size, not of the
colour.
"""
scale = page.dpi / 72.0
left, top = int(word[1] * scale), int(word[2] * scale)
right, low = int(word[3] * scale) + 1, int(word[4] * scale) + 1
return min(page.pixels[y * page.width + x]
for y in range(max(top, 0), min(low, page.height))
for x in range(max(left, 0), min(right, page.width)))
def tagline_colour(check):
"""A company that names no colour for its second line sets none.
The tagline then stands in the colour the name over it stands in, because
the lockup sets its colour once and no line of the block sets one of its
own. Named, the colour reaches that line alone and the name keeps the one
of the lockup. Measured at the darkest pixel of each word: the quiet role
of the example company is a 55 percent tint of the brand colour, so the two
lines come out far apart where it is named and together where it is not.
"""
tone = {}
for name, fixture in (("quiet", TAGLINE_QUIET), ("plain", TAGLINE_PLAIN)):
source = support.write("tagcolour%s.tex" % name, fixture)
pdf, _ = build(source, EXAMPLES)
page = Page(pdf, 2)
found = {word[0]: word for word in support.words(pdf, 2)
if word[4] < 90}
check.that("Nordwind" in found and "Vernunft" in found,
f"the head of the {name} company carries no lockup")
if "Nordwind" not in found or "Vernunft" not in found:
return
tone[name] = (darkest(page, found["Nordwind"]),
darkest(page, found["Vernunft"]))
check.that(abs(tone["plain"][0] - tone["plain"][1]) < 12,
f"without a colour of its own the tagline comes out at "
f"{tone['plain'][1]} against the {tone['plain'][0]} of the name: "
"a colour was set for that line")
check.that(tone["quiet"][1] - tone["quiet"][0] > 30,
f"the named colour reached the tagline at {tone['quiet'][1]} "
f"against the {tone['quiet'][0]} of the name, and the quiet role "
"of the company is far lighter than its brand colour")
check.that(abs(tone["quiet"][0] - tone["plain"][0]) < 12,
"a colour named for the tagline changed the colour of the name")
def main():
check = Check("identity")
source_checks(check)
plain(check)
page_checks(check)
logo_from_file(check)
tagline(check)
tagline_colour(check)
lockup_sizes(check)
return check.done()
if __name__ == "__main__":
sys.exit(main())
+747
View File
@@ -0,0 +1,747 @@
#!/usr/bin/env python3
"""The page, measured where the reader looks at it.
Two things are measured, and the second is the one the family has paid for
three times: an announcement and what it announces stand on the same page. The
fixture produces that situation itself, over every distance to the page foot,
instead of hoping that a document happens to carry it.
"""
import os
import re
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import support
from support import Check, Page, build
EXAMPLES = os.path.join(support.ROOT, "examples")
HEAD = r"""\documentclass[a4paper,10pt]{article}
\usepackage{nordwind}
\begin{document}
"""
# Every distance from a full page to none: the announcement is pushed down the
# page line by line, and at some of those distances it used to stay behind alone
# at the foot while what it announced began overleaf.
CASES = 16
def table(marker):
return ("\\begin{longtable}{ll}\n\\toprule\n"
"\\businesstablehead{Name} & \\businesstablehead{Value}\\\\\n"
"\\midrule\n\\endhead\n"
"%s & 1\\\\ \\businessrowrule\n"
"Second row & 2\\\\\n\\bottomrule\n\\end{longtable}\n" % marker)
def long_table(marker):
"""A table that asks for much more room than a heading does by itself."""
rows = "".join("Row %d & %d\\\\ \\businessrowrule\n" % (number, number)
for number in range(12))
return ("\\begin{longtable}{ll}\n\\toprule\n"
"\\businesstablehead{Name} & \\businesstablehead{Value}\\\\\n"
"\\midrule\n\\endhead\n"
"%s & 1\\\\ \\businessrowrule\n%s"
"Last row & 2\\\\\n\\bottomrule\n\\end{longtable}\n"
% (marker, rows))
def pair_document():
"""A heading directly under a heading, over every distance to the foot.
Two headings that follow one another are one announcement: the second names
a part of what the first names, and a page that breaks between them carries
a heading and nothing else.
"""
parts = [HEAD]
for case in range(CASES):
parts.append("\\newpage\n")
parts.append("Filler line.\\par\n" * case)
parts.append("\\section{Pair case %d}\n" % case)
parts.append("\\subsection{Under case %d}\n" % case)
parts.append("Lorem ipsum dolor sit amet.\\par\n" * 3)
# And the same with something under the second heading that asks for much
# more room than a heading does by itself: there the reservation of the
# second one breaks the page, and the first is left behind alone.
for case in range(CASES):
parts.append("\\newpage\n")
parts.append("Filler line.\\par\n" * case)
parts.append("\\section{Deep case %d}\n" % case)
parts.append("\\subsection{Deeper case %d}\n" % case)
parts.append(long_table("Deep cell case %d" % case))
# And with a picture under the second heading: a picture measures nothing
# into the aux file, so the reservation of the heading is its own eight
# lines, and what breaks the page is the picture that does not fit.
for case in range(CASES):
parts.append("\\newpage\n")
parts.append("Filler line.\\par\n" * case)
parts.append("\\section{Shown case %d}\n" % case)
parts.append("\\subsection{Picture case %d}\n" % case)
parts.append("\\begin{figure}[H]\\centering\n"
"\\includegraphics[height=0.55\\textheight]"
"{example-image}\n"
"\\caption{Figure case %d}\n\\end{figure}\n" % case)
parts.append("\\end{document}\n")
return "".join(parts)
def sweep_document():
parts = [HEAD]
for case in range(CASES):
parts.append("\\newpage\n")
parts.append("Filler line.\\par\n" * case)
parts.append("\\section{Heading case %d}\n" % case)
parts.append("\\businessleadin[1]\nThe numbers:\n\\businesstogether\n")
parts.append(table("Heading cell case %d" % case))
for case in range(CASES):
parts.append("\\newpage\n")
parts.append("Filler line.\\par\n" * case)
parts.append("\\businessleadin[1]\nSentence case %d:\n\\businesstogether\n"
% case)
parts.append(table("Sentence cell case %d" % case))
parts.append("\\end{document}\n")
return "".join(parts)
# The title rule, measured between three bands of ink: a paragraph, the heading,
# a paragraph. The page carries nothing else, so which band is which is known
# and not guessed.
RULE = r"""\documentclass[a4paper,10pt]{article}
\usepackage[nologo]{nordwind}
\begin{document}
\pagestyle{empty}
Lorem ipsum before the heading.
\section{Consetetur}
Dolor sit amet after the heading.
\end{document}
"""
# The head of a page: a heading on the left, the logo with the name on the
# right. The two texts are of very different size, so any alignment but the one
# on the text line is visible at once.
HEADLINE = r"""\documentclass[a4paper,10pt]{article}
\usepackage{nordwind}
%s\begin{document}
\section{Lorem}
Dolor sit amet.
\end{document}
"""
HEADLINE_ONE = HEADLINE % ""
# The same head with the lockup of two lines. The heading on the left has to
# stand on the line of the NAME, the first of the two, and not on the middle of
# a block that has grown a second line.
HEADLINE_TWO = HEADLINE % (
"\\businessidentity{tagline={Vernunft und Freiheit}}\n")
# The three pictures of the rule, each alone on a page of its own: one taller
# than wide, one wider than tall, and a raster of pixels that is smaller than
# the page and may not be blown up past twice its size.
PICTURES = r"""\documentclass[a4paper,10pt]{article}
\usepackage{nordwind}
\begin{document}
\pagestyle{empty}
\businessgraphic{example-image-9x16}
\newpage
\businessgraphic{example-image-16x9}
\newpage
\businessgraphic{example-image-a.png}
\end{document}
"""
# The same raster picture in a document that allows it a tenth of enlargement
# and no more: 401.5 points wide by itself, so 441.6 with the option below,
# where the line would otherwise give it 540.6.
ZOOM = r"""\documentclass[a4paper,10pt]{article}
\usepackage[picturezoom=1.1]{nordwind}
\begin{document}
\businessgraphic{example-image-a.png}
\end{document}
"""
# A picture that does not fit into what is left of the page: half a page of
# text, then a picture that would take a whole one.
PICTUREROOM = r"""\documentclass[a4paper,10pt]{article}
\usepackage{nordwind}
\begin{document}
\pagestyle{empty}
""" + "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.\\par\n" * 28 + r"""
\businessgraphic{example-image-16x9}
\end{document}
"""
# A document that opens with a heading, and the same one that opens with a
# paragraph: the distance under the title block belongs to the block and is the
# same in both.
OPENING = r"""\documentclass[a4paper,10pt]{article}
\usepackage{nordwind}
\title{Lorem Ipsum}
\author{Erika Muster}
\date{21 September 2026}
\begin{document}
\maketitle
%s
\end{document}
"""
OPENING_HEADING = OPENING % "\n\\section{Consetetur}\n\nDolor sit amet.\n"
OPENING_TEXT = OPENING % "\nDolor sit amet.\n"
# The inner corner of the foot, with something in it that is taller than the
# line the foot reserves. Without the measurement that gives the difference to
# the foot, a logo of two centimetres is drawn into the text above it.
FOOT = r"""\documentclass[a4paper,10pt]{article}
\usepackage{nordwind}
\businessfootleft{\includegraphics[height=2cm]{example-image}}
\begin{document}
\section{Consetetur}
""" + "Lorem ipsum dolor sit amet, consetetur sadipscing.\\par\n" * 40 + r"""
\end{document}
"""
# The two distances of the page, asked of the page itself. What a document may
# put in the foot changes how much room the foot needs, so the fixture is built
# twice: once with the bare line of the foot, once with a picture of two
# centimetres in it. The numbers come out of the run because they ARE the
# definition — the page is laid out from them, and a distance read off the
# raster carries the slack of whichever letters happen to stand at the edge.
EDGES = r"""\documentclass[a4paper,10pt]{article}
\usepackage{nordwind}
%s\makeatletter
\AtBeginDocument{%%
\typeout{EDGES line=\the\baselineskip}%%
\typeout{EDGES depth=\the\dp\strutbox}%%
\typeout{EDGES topskip=\the\topskip}%%
\typeout{EDGES headsep=\the\headsep}%%
\typeout{EDGES footskip=\the\footskip}%%
\typeout{EDGES foot=\the\business@foothigh}}
\makeatother
\begin{document}
\section{Consetetur}
""" + "Lorem ipsum dolor sit amet, consetetur sadipscing.\\par\n" * 40 + r"""
\end{document}
"""
# A company whose mark is taller than the family's. The head has to keep the
# room for it, and everything under the head has to move down by what it took:
# the head is MEASURED at the head, and a document that asks for another height
# is the only thing that proves the measurement is one.
HEADROOM = r"""\documentclass[a4paper,10pt]{article}
\usepackage[logoheight=%s]{nordwind}
\begin{document}
\section{Lorem}
Dolor sit amet.
\end{document}
"""
EDGES_LINE = EDGES % ""
EDGES_PICTURE = EDGES % \
"\\businessfootleft{\\includegraphics[height=2cm]{example-image}}\n"
def bands(page):
"""The rows of ink, grouped into the blocks they form."""
rows = page.ink_rows()
grouped, current = [], []
for row in rows:
if current and row - current[-1] > 1:
grouped.append(current)
current = []
current.append(row)
if current:
grouped.append(current)
return grouped
def page_of(pdf, needle, count):
"""The page a text stands on, counted from one; zero where it is missing."""
for number in range(1, count + 1):
found = support.run(["pdftotext", "-f", str(number), "-l", str(number),
pdf, "-"]).stdout
if needle in found:
return number
return 0
def title_rule(check):
source = support.write("titlerule.tex", RULE)
pdf, _ = build(source, EXAMPLES)
page = Page(pdf, 1)
blocks = bands(page)
check.that(len(blocks) == 3,
f"the fixture should carry three blocks of ink, it carries "
f"{len(blocks)}")
if len(blocks) != 3:
return
factor = 72.0 / page.dpi
above = (blocks[1][0] - blocks[0][-1]) * factor
below = (blocks[2][0] - blocks[1][-1]) * factor
# The two distances of the family are one line and a half over a heading and
# half a line under it, which is the three to one the rule asks for. What
# the page shows is that glue plus the slack of the two faces that meet at
# it — the descender of the paragraph, the capitals of the heading — and
# that slack is under half a line for every face this suite has been set
# in. Measured in the report of the origin, the document this family works
# from: 19.7 points over a heading of the second level and 9.1 under it, at
# a line of twelve points.
line = 12.0
check.that(1.25 * line < above < 2.0 * line,
f"the room over the heading is {above:.1f} points, and one line "
f"and a half of {line:.0f} points with the slack of the faces is "
"between 15 and 24")
check.that(0.4 * line < below < 1.1 * line,
f"the room under the heading is {below:.1f} points, and half a "
f"line of {line:.0f} points with the slack of the faces is "
"between 5 and 13")
check.that(above > below,
f"the room over the heading is {above:.1f} points and under it "
f"{below:.1f}: a heading belongs to what follows it")
def left_edge(check):
pdf, _ = build(os.path.join(EXAMPLES, "sample.tex"), EXAMPLES)
page = Page(pdf, 1)
edges = []
for band in bands(page):
columns = page.ink_columns(band[0], band[-1] + 1)
if columns:
edges.append(columns[0])
left = min(edges)
# A paragraph of this design begins at the margin, and so does a heading.
# The bands that legitimately begin further right are the ones the layout
# puts there: the folio at the foot, the logo and the address in the head,
# and the cells of a table.
off = [edge for edge in edges if edge - left > 4]
check.that(len(off) <= len(edges) / 2,
f"{len(off)} of {len(edges)} blocks do not begin on the left "
"edge of the text")
def together(check):
source = support.write("sweep.tex", sweep_document())
pdf, _ = build(source, EXAMPLES)
count = support.pages(pdf)
for case in range(CASES):
heading = page_of(pdf, "Heading case %d" % case, count)
cell = page_of(pdf, "Heading cell case %d" % case, count)
check.that(heading and cell and heading == cell,
f"heading case {case}: the heading stands on page {heading} "
f"and its table on page {cell}")
for case in range(CASES):
sentence = page_of(pdf, "Sentence case %d:" % case, count)
cell = page_of(pdf, "Sentence cell case %d" % case, count)
check.that(sentence and cell and sentence == cell,
f"sentence case {case}: the sentence stands on page "
f"{sentence} and its table on page {cell}")
def foot(check):
"""What a document puts in the inner corner of the foot stays in the foot."""
source = support.write("foot.tex", FOOT)
pdf, _ = build(source, EXAMPLES)
page = Page(pdf, 1)
blocks = bands(page)
check.that(len(blocks) >= 2, "the page carries no foot")
if len(blocks) < 2:
return
text, standing = blocks[-2], blocks[-1]
factor = 72.0 / page.dpi
gap = (standing[0] - text[-1]) * factor
check.that(gap > 0,
f"the foot overprints the text above it by {-gap:.1f} points")
below = (page.height - standing[-1]) * factor
check.that(14 < below < 57,
f"the foot stands {below:.1f} points from the lower edge of the "
"paper, outside the margin the page keeps")
# And it is really the picture that stands there, not a line of text: a
# block of two centimetres is 56.9 points high.
high = (standing[-1] - standing[0] + 1) * factor
check.that(high > 50,
f"the foot block is {high:.1f} points high, so what the document "
"put there did not arrive")
def edges(check):
"""One line of the text between the head and the text, and at the foot.
The head, the text and the foot are three blocks on one sheet, and the two
distances between them are the same: exactly one line of the body text,
whatever stands in the head and in the foot. Three lengths say it.
`\\headsep` is that distance over the text, which is how LaTeX defines it:
from the lower edge of the head box to the upper edge of the text block.
`\\topskip` says where the FIRST line of a page stands in that block, and
LaTeX sets it to a round ten points while a line stands `\\ht\\strutbox`
over its own baseline. The block would then begin over its first line and
the distance the page defines would not be the one a reader measures.
Under the text there is no such length: `\\footskip` runs from the baseline
of the last line to the baseline of the foot, so the foot itself and what a
line hangs below its baseline lie inside it. Both come off, and what is left
is the line.
"""
for name, fixture, what in (("edgeline", EDGES_LINE, "a bare line"),
("edgefoot", EDGES_PICTURE, "a picture")):
source = support.write(name + ".tex", fixture)
pdf, log = build(source, EXAMPLES)
with open(log, encoding="utf-8") as handle:
found = dict(re.findall(r"EDGES (\w+)=([0-9.]+)pt", handle.read()))
check.that(len(found) == 6,
f"the fixture with {what} in the foot reported "
f"{len(found)} of the six lengths of the page")
if len(found) != 6:
continue
line, depth = float(found["line"]), float(found["depth"])
over = float(found["headsep"])
under = float(found["footskip"]) - float(found["foot"]) - depth
first = float(found["topskip"])
check.that(abs(over - line) < 0.01,
f"with {what} in the foot the head stands {over:.2f} points "
f"over the text, and a line is {line:.2f}")
check.that(abs(under - line) < 0.01,
f"with {what} in the foot the text stands {under:.2f} points "
f"over the foot, and a line is {line:.2f}")
check.that(abs(first - (line - depth)) < 0.01,
f"the first line of a page begins {first:.2f} points under "
f"the upper edge of the text block, and a line stands "
f"{line - depth:.2f} over its own baseline")
# And the page shows it: between the last line of the text and what
# stands in the foot there is a line, plus whatever that last line does
# not hang below its baseline. Never less, and never a second line.
page = Page(pdf, 1)
blocks = bands(page)
check.that(len(blocks) >= 2,
f"the page with {what} in the foot carries no foot")
if len(blocks) < 2:
continue
factor = 72.0 / page.dpi
gap = (blocks[-1][0] - blocks[-2][-1]) * factor
check.that(line - 0.5 < gap < line + depth + 0.5,
f"with {what} in the foot the ink of the text stands "
f"{gap:.1f} points over the ink of the foot, and a line of "
f"{line:.0f} points with the depth of a line is between "
f"{line:.0f} and {line + depth:.1f}")
def headroom(check):
"""A mark of another height, and the page that keeps the room for it.
The head reserves what it measures at itself, and that is only proven by a
document which asks for a height the family did not set: at the one height
every example carries, a reservation computed from a factor and one taken
from the box look exactly alike. Two heights are built, the family's and
twice it, and three things are measured — the log stays silent, the head
grows with the mark, and the text under it moves down by what the head took.
Reported on 2026-09-22 from a sibling template of this family, which found
the same gap in its own suite: the case had been reasoned about in the
source and never built.
"""
seen = {}
for name, height in (("small", "2em"), ("large", "4em")):
source = support.write("headroom%s.tex" % name, HEADROOM % height)
pdf, log = build(source, EXAMPLES)
with open(log, encoding="utf-8") as handle:
printed = handle.read()
check.that("headheight is too small" not in printed,
f"with a mark of {height} the page kept less room for the "
"head than the head takes")
reserved = {key: float(value) for key, value in
re.findall(r"^\* \\(headheight|headsep|topmargin)"
r"=(-?[0-9.]+)pt$", printed, re.MULTILINE)}
check.that(len(reserved) == 3,
f"with a mark of {height} the page did not report its "
"layout")
if len(reserved) != 3:
return
# Where the text block begins, out of the numbers the page was laid out
# from: an inch of driver offset, the margin, the room for the head and
# the distance under it. TeX points against the PostScript points of the
# raster, which are the shorter by the ratio of the two inches.
top = (72.27 + reserved["topmargin"] + reserved["headheight"]
+ reserved["headsep"]) * 72.0 / 72.27
page = Page(pdf, 1)
factor = 72.0 / page.dpi
blocks = bands(page)
above = [band for band in blocks if band[-1] * factor < top]
below = [band for band in blocks if band[0] * factor >= top - 1]
check.that(len(above) + len(below) == len(blocks),
f"with a mark of {height} a block of ink crosses the lower "
"edge of the head")
check.that(above and below,
f"with a mark of {height} the page carries no head or no "
"text")
if not above or not below:
return
seen[name] = (reserved["headheight"], below[0][0] * factor, top)
if len(seen) != 2:
return
# The mark doubles, so the room the head keeps grows with it — by about the
# same twenty points, and never by nothing.
grown = seen["large"][0] - seen["small"][0]
check.that(grown > 15,
f"the mark doubled and the room for the head grew by "
f"{grown:.1f} points, so the room does not follow the mark")
moved = seen["large"][1] - seen["small"][1]
check.that(moved > 15,
f"the text under the taller head begins {moved:.1f} points "
"lower, so the page did not give the head what it took")
def first_glyph(page, top, bottom, left, right):
"""The column range of the leftmost letter in a rectangle of the page."""
columns = [x for x in range(left, right)
if any(page.dark(x, y) for y in range(top, bottom))]
if not columns:
return None
start = end = columns[0]
for column in columns[1:]:
if column - end > 2:
break
end = column
return start, end + 1
def bottom_row(page, top, bottom, left, right):
rows = [y for y in range(top, bottom)
if any(page.dark(x, y) for x in range(left, right))]
return rows[-1] if rows else None
def head_line(check):
"""The two texts of the head stand on ONE line.
The heading on the left is nine points, the name beside the logo sixteen,
so the two carry every alignment differently: on a common top edge their
baselines fall apart by the difference of the two cap heights, and the head
reads as two blocks that missed each other. What is measured is the lower
edge of the first capital on each side — the L of the heading and the N of
the name, both flat on the baseline.
The tolerance is the rasterizer's own: a capital of sixteen points and one
of nine end within a third of a point of each other at 300 dpi, while the
defect this catches was sixteen points, the whole height of the name.
It is measured over both shapes of the lockup, the one of one line and the
one of two: a block that has grown a second line still hands the head the
baseline of its FIRST, or the heading on the left drops to the middle of a
block it knows nothing about.
"""
for what, fixture, lines in (("one line", HEADLINE_ONE, 1),
("two lines", HEADLINE_TWO, 2)):
source = support.write("headline%d.tex" % lines, fixture)
pdf, _ = build(source, EXAMPLES)
page = Page(pdf, 1, dpi=300)
# The topmost block of ink on the page IS the line of the head: the icon
# reaches above and below the texts beside it, so the heading, the name
# and the logo form one band. The address line under the logo and the
# text of the page are the blocks after it.
blocks = bands(page)
check.that(len(blocks) >= 2, f"the page with {what} carries no head")
if len(blocks) < 2:
continue
top, end = blocks[0][0], blocks[0][-1]
middle = page.width // 2
# On the right the first group of columns is the icon and the second the
# name; on the left the first is the first letter of the heading.
icon = first_glyph(page, top, end + 1, middle, page.width)
check.that(icon, f"the head with {what} carries no logo")
if not icon:
continue
# Right of the icon stand the lines of the block, and there they are
# separate bands: the icon itself bridges them, because it is one shape
# as tall as both. The name is the first of them.
text = [y for y in range(top, end + 1)
if any(page.dark(x, y) for x in range(icon[1] + 2, page.width))]
rows, current = [], []
for row in text:
if current and row - current[-1] > 1:
rows.append(current)
current = []
current.append(row)
if current:
rows.append(current)
check.that(len(rows) == lines,
f"the block beside the logo carries {len(rows)} lines and "
f"should carry {lines}")
if not rows:
continue
first, last = rows[0][0], rows[0][-1] + 1
name = first_glyph(page, first, last, icon[1] + 2, page.width)
heading = first_glyph(page, first, last, 0, middle)
check.that(name and heading,
f"the head with {what} carries no name or no heading")
if not name or not heading:
continue
left_base = bottom_row(page, first, last, heading[0], heading[1])
right_base = bottom_row(page, first, last, name[0], name[1])
apart = abs(left_base - right_base) * 72.0 / page.dpi
check.that(apart < 1,
f"with {what} in the lockup the heading in the head and the "
f"name beside the logo stand {apart:.1f} points apart and "
"not on one line")
def opening(check):
"""The title block brings the distance under it, and nothing adds to it.
A heading brings three lines of space with it and the title block does too.
Where a document opens with a heading, the two used to stand one under the
other: 76.8 points between the date and the heading, against the 44.2 the
same block gives a paragraph. Six lines of white where the page gives three
everywhere else.
"""
gaps = []
for name, source in (("openheading", OPENING_HEADING),
("opentext", OPENING_TEXT)):
pdf, _ = build(support.write(name + ".tex", source), EXAMPLES)
page = Page(pdf, 1)
blocks = bands(page)
check.that(len(blocks) >= 6, f"{name}: the page carries no title block")
if len(blocks) < 6:
return
factor = 72.0 / page.dpi
# The bands of this page: the head, the address line under the logo,
# the title, the author, the date — and then what the document opens
# with, which is what this measures.
gaps.append((blocks[5][0] - blocks[4][-1]) * factor)
apart = abs(gaps[0] - gaps[1])
check.that(apart < 6,
f"a document that opens with a heading gives {gaps[0]:.1f} "
f"points under the title block and one that opens with a "
f"paragraph {gaps[1]:.1f}, which is {apart:.1f} points apart")
def pairs(check):
"""No page break between two headings that follow one another."""
source = support.write("pairs.tex", pair_document())
pdf, _ = build(source, EXAMPLES)
count = support.pages(pdf)
for case in range(CASES):
upper = page_of(pdf, "Pair case %d" % case, count)
lower = page_of(pdf, "Under case %d" % case, count)
check.that(upper and lower and upper == lower,
f"pair case {case}: the heading stands on page {upper} and "
f"the heading under it on page {lower}")
for case in range(CASES):
upper = page_of(pdf, "Deep case %d" % case, count)
lower = page_of(pdf, "Deeper case %d" % case, count)
cell = page_of(pdf, "Deep cell case %d" % case, count)
check.that(lower and cell and lower == cell,
f"deep case {case}: the heading stands on page {lower} and "
f"its table on page {cell}")
for case in range(CASES):
upper = page_of(pdf, "Shown case %d" % case, count)
lower = page_of(pdf, "Picture case %d" % case, count)
shown = page_of(pdf, "Figure case %d" % case, count)
check.that(upper and lower and upper == lower,
f"picture case {case}: the heading stands on page {upper} "
f"and the heading under it on page {lower}")
check.that(lower and shown and lower == shown,
f"picture case {case}: the heading stands on page {lower} "
f"and its picture on page {shown}")
check.that(upper and lower and upper == lower,
f"deep case {case}: the heading stands on page {upper} and "
f"the heading under it on page {lower}")
def pictures(check):
"""What a picture takes of the page, measured at the ink on the page.
Upright: the full width of the line, at most half the height of the text.
Lying: the full height that is free, at most the full width. A raster of
pixels is enlarged twice over and no further. Every one of them centred.
"""
pdf, _ = build(support.write("pictures.tex", PICTURES), EXAMPLES)
for number, (name, rule) in enumerate((
("upright", "height"), ("lying", "width"),
("raster", "zoom")), start=1):
page = Page(pdf, number)
factor = 72.0 / page.dpi
# The page carries the head, the picture and the folio; the picture is
# the tallest block of ink on it.
blocks = bands(page)
check.that(blocks, f"the {name} picture is not on its page")
if not blocks:
continue
tallest = max(blocks, key=lambda band: band[-1] - band[0])
top, bottom = tallest[0], tallest[-1]
columns = page.ink_columns(top, bottom + 1)
width = (columns[-1] - columns[0] + 1) * factor
height = (bottom - top + 1) * factor
left = columns[0] * factor
right = page.width * factor - columns[-1] * factor
# A4 is 597.5 points wide, the text block 540.6 with 28.35 on each
# side, and the text is 713.9 points high.
if rule == "height":
check.that(abs(height - 0.5 * 713.9) < 12,
f"the upright picture is {height:.1f} points high and "
"not the half of the text height it may take")
if rule == "width":
check.that(abs(width - 540.6) < 4,
f"the lying picture is {width:.1f} points wide against "
"the 540.6 of the line")
if rule == "zoom":
# 401.5 points wide by itself, and the line gives 540.6, which is
# 1.35 of it — under the two a raster may be enlarged by.
check.that(abs(width - 540.6) < 4,
f"the raster picture is {width:.1f} points wide against "
"the 540.6 of the line")
check.that(abs(left - right) < 4,
f"the {name} picture stands {left:.1f} points from the left "
f"edge and {right:.1f} from the right, so it is not centred")
# A raster picture is enlarged as far as the document allows and no
# further, which is what the option says.
zoom, _ = build(support.write("zoom.tex", ZOOM), EXAMPLES)
page = Page(zoom, 1)
factor = 72.0 / page.dpi
blocks = bands(page)
tallest = max(blocks, key=lambda band: band[-1] - band[0])
columns = page.ink_columns(tallest[0], tallest[-1] + 1)
width = (columns[-1] - columns[0] + 1) * factor
check.that(abs(width - 441.6) < 4,
f"the raster picture is {width:.1f} points wide, and a tenth "
"over its own 401.5 is 441.6")
# And the picture that does not fit into what is left of the page stays on
# it, small enough to fill what is there.
room, _ = build(support.write("pictureroom.tex", PICTUREROOM), EXAMPLES)
check.that(support.pages(room) == 1,
f"the page with the picture broke into {support.pages(room)} "
"pages instead of fitting the picture into what was left")
if support.pages(room) == 1:
page = Page(room, 1)
factor = 72.0 / page.dpi
tallest = max(bands(page), key=lambda band: band[-1] - band[0])
high = (tallest[-1] - tallest[0] + 1) * factor
# At the full width of the line the picture is 304 points high, and
# what was left of the page is less than that.
check.that(high < 300,
f"the picture is {high:.1f} points high, so it was not made "
"small enough for the room that was left")
def main():
check = Check("layout")
title_rule(check)
pairs(check)
pictures(check)
head_line(check)
opening(check)
edges(check)
headroom(check)
foot(check)
left_edge(check)
together(check)
return check.done()
if __name__ == "__main__":
sys.exit(main())
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Every build log of every document, read for what a green run hides.
A build that ends with a PDF has not said that it went well. LaTeX reports a
box that runs over the edge, a character its font does not carry and a head
that is too small for what stands in it, and then writes the page anyway. An
error that stands in every log hides the next one that means something.
"""
import os
import re
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import support
from support import Check, build
EXAMPLES = os.path.join(support.ROOT, "examples")
DOCUMENTS = ("sample.tex", "logo-example.tex", "letter.tex",
"presentation.tex")
# What no log of this suite may carry, and why each one matters.
COMPLAINTS = (
# An error stops nothing in nonstopmode: the run goes on and the page comes
# out with the defect on it.
(re.compile(r"^! ", re.MULTILINE), "an error"),
# A box that runs over the edge is text outside the paper.
(re.compile(r"^Overfull \\hbox", re.MULTILINE), "an overfull box"),
# A character the font does not carry prints NOTHING, with no gap a reader
# would notice and no error.
(re.compile(r"Missing character", re.MULTILINE), "a missing character"),
# A font shape LaTeX does not find is replaced silently, and the page then
# carries a face nobody chose.
(re.compile(r"Font shape .* undefined", re.MULTILINE),
"an undefined font shape"),
(re.compile(r"Some font shapes were not available", re.MULTILINE),
"a substituted font"),
# A head that is too small for what stands in it overprints the text under
# it, and fancyhdr says so once per page.
(re.compile(r"headheight is too small", re.MULTILINE),
"a head that is too small"),
# And a foot that is too small is the same defect at the other edge: the
# page number and whatever the document put beside it are drawn into the
# text above them. fancyhdr says so once per page.
(re.compile(r"footskip is too small", re.MULTILINE),
"a foot that is too small"),
# A title block without an author prints the block and leaves the line out,
# and the document says nothing about who wrote it.
(re.compile(r"No \\author given", re.MULTILINE),
"a title block without an author"),
(re.compile(r"Package hyperref Warning", re.MULTILINE),
"a warning of hyperref"),
)
def logs():
"""Every log THIS RUN produced, from the examples and from the fixtures.
Only the logs under the test build: a log beside the examples was written
by whatever state the working tree had when somebody last built there, and
a defect that was repaired an hour ago still stands in it. Measured on
2026-09-21, when a warning that the fresh build no longer carries was
reported from a log of the build before it.
The probe of the converter is not among them either: it is run once, on
purpose, to measure the width of a line, and it produces no page for any
reader. Its log therefore always asks for the second run that a document
gets and a measurement does not need.
"""
found = []
for directory, _, names in os.walk(support.BUILD):
for name in names:
if name.endswith(".log") and name != "metrics.log":
found.append(os.path.join(directory, name))
return sorted(found)
def main():
check = Check("logs")
for name in DOCUMENTS:
source = os.path.join(EXAMPLES, name)
if os.path.exists(source):
build(source, EXAMPLES)
read = logs()
check.that(read, "no build log was found at all")
for path in read:
with open(path, encoding="utf-8", errors="replace") as handle:
text = handle.read()
for pattern, what in COMPLAINTS:
hit = pattern.search(text)
check.that(hit is None,
f"{os.path.basename(path)} reports {what}: "
f"{text[hit.start():hit.start() + 120].splitlines()[0] if hit else ''}")
print(f" {len(read)} logs read")
return check.done()
if __name__ == "__main__":
sys.exit(main())
+340
View File
@@ -0,0 +1,340 @@
#!/usr/bin/env python3
"""The way from Markdown to the PDF of a company, measured at the PDF.
The converter is the second road into the same design, and the two have to end
in the same page: the same font, the same logo, the same table that fits the
line. What is measured here is the result and not the call.
"""
import os
import re
import shutil
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import support
from support import Check, Page
EXAMPLES = os.path.join(support.ROOT, "examples")
CONVERTER = os.path.join(support.ROOT, "bin", "md2pdf.py")
OUT = os.path.join(support.BUILD, "md2pdf")
def runnable(check):
"""The converter is called by its name, so it has to be executable.
The README describes exactly that way — a link in a directory of the PATH —
and a file without the bit ends in "Permission denied". Reported on
2026-09-21 from a company that had linked it and called it.
"""
check.that(os.access(CONVERTER, os.X_OK),
"bin/md2pdf.py carries no execute bit, so a link to it on the "
"PATH cannot be called by its name")
def convert(check):
shutil.rmtree(OUT, ignore_errors=True)
os.makedirs(OUT, exist_ok=True)
result = support.run(["python3", CONVERTER, "--identity",
"nordwind", "--out-dir", OUT, "--keep-tex",
os.path.join(EXAMPLES, "report.md")])
pdf = os.path.join(OUT, "report.pdf")
check.that(os.path.exists(pdf),
f"the converter produced no PDF:\n{result.stdout[-1500:]}")
return pdf
def measured(check):
"""The line and the character were measured, not taken from the fallback.
The numbers the filters compute a column width from belong to the paper, the
margins and the face of the company. They live outside TeX, in a Lua filter,
so the converter builds a probe with the identity of the company and reads
them out of it. Without the probe every company would compute with the
numbers of the company this suite came from.
"""
metrics = os.path.join(OUT, "build", "metrics.txt")
check.that(os.path.exists(metrics),
"the converter wrote no measurement of the line")
if not os.path.exists(metrics):
return
with open(metrics, encoding="utf-8") as handle:
found = dict(re.findall(r"^(\w+) ([0-9.]+)pt$", handle.read(),
re.MULTILINE))
check.that("linewidth" in found, "the probe measured no line width")
if "linewidth" not in found:
return
# A4 is 597.5 points wide and the template keeps 1cm on each side, so the
# line is 540 points give or take the rounding of the paper.
width = float(found["linewidth"])
check.that(530 < width < 545,
f"the measured line is {width} points wide, which is not the "
"type area of an A4 page with the margins of this template")
def inside(check, pdf):
"""Nothing of the document stands outside the type area.
A Markdown table says nothing about its column widths, and without the
filter LaTeX writes the last words of a wide table past the edge of the
paper.
"""
page = Page(pdf, 1)
factor = 72.0 / page.dpi
columns = page.ink_columns()
right = columns[-1] * factor
left = columns[0] * factor
# The paper is 595 points wide and the template keeps 1cm, 28.35 points, on
# each side. Half a point of tolerance for the rounding of the raster.
check.that(left > 27, f"ink stands {left:.1f} points from the left edge, "
"inside the margin")
check.that(right < 568, f"ink reaches {right:.1f} points, past the right "
"margin of the page")
def content(check, pdf):
text = support.text(pdf)
check.that("Lorem Ipsum" in text, "the title block did not arrive")
check.that("Consetetur sadipscing elitr" in text and "41'820" in text,
"the table did not arrive")
# The path in the running text is broken at the characters that structure
# it, so it stays inside the line. What arrives in the text layer is the
# path with its pieces, never a line that runs past the margin.
check.that("business-identity.sty" in text.replace("\n", ""),
"the path in the running text did not arrive")
faces = support.fonts(pdf)
check.that(any("DejaVu" in face for face in faces),
f"the marks did not reach the page: {faces}")
# A formula in the three ways a writer writes one, and a fourth that is none:
# inside a fenced code block the dollars are an example of themselves and stay
# on the page as text.
FORMULA = """---
title: Formula
author: Erika Muster
---
# Consetetur
Inline $E = mc^2$ in a sentence.
$$a = \\frac{b}{c}$$
$$
\\text{Ratio}
=
\\frac{\\text{Nutzen}}
{\\text{Kosten}}
$$
```text
$$
x
=
y
$$
```
"""
def formula(check):
"""A formula arrives as a formula, also written over several lines.
The grammar of the reader sees a line carrying nothing but `=` as the
underline of a heading, so a formula over several lines used to come out as
a heading of the first level with the dollars in it, and the build stayed
green. Reported on 2026-09-21 from another company of this family, which
built a twelve-page analysis with the formula standing as a green heading.
"""
path = os.path.join(support.BUILD, "formula.md")
os.makedirs(support.BUILD, exist_ok=True)
with open(path, "w", encoding="utf-8") as handle:
handle.write(FORMULA)
result = support.run(["python3", CONVERTER, "--identity", "nordwind",
"--out-dir", OUT, "--keep-tex", path])
pdf = os.path.join(OUT, "formula.pdf")
check.that(os.path.exists(pdf),
f"the document with the formula did not build:\n"
f"{result.stdout[-1500:]}")
if not os.path.exists(pdf):
return
text = support.text(pdf)
# The dollars of a formula never reach the page; the ones in the code block
# do, and they are the proof that nothing was repaired in there.
body = text.split("$$")[0] if "$$" in text else text
check.that(text.count("$$") == 2,
f"the page carries {text.count('$$')} rows of dollars, and only "
"the two of the code block belong there")
check.that("Ratio" in body and "Nutzen" in body,
"the formula over several lines did not arrive as a formula")
check.that("Kosten" in body, "the second half of the formula is missing")
faces = support.fonts(pdf)
check.that(any("Math" in face or "math" in face for face in faces),
f"no mathematics face on a page with three formulas: {faces}")
# The same document twice: once written in Markdown, once in LaTeX. A document
# looks the same whichever road it takes, so the two pages carry their ink in
# the same places.
TWIN_BODY = (
("Consetetur", "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, "
"sed diam nonumy eirmod tempor invidunt ut labore et dolore magna "
"aliquyam erat."),
("Dolor Sit", "Stet clita kasd gubergren, no sea takimata sanctus est "
"Lorem ipsum dolor sit amet."),
("Sadipscing Elitr", "At vero eos et accusam et justo duo dolores et ea "
"rebum."),
)
TWIN_MD = "---\ntitle: Lorem Ipsum\nauthor: Erika Muster\ndate: 21 September 2026\n---\n\n" \
+ "\n".join(f"{'#' * (level + 1)} {head}\n\n{text}\n"
for level, (head, text) in enumerate(TWIN_BODY))
TWIN_TEX = r"""\documentclass[a4paper,10pt]{article}
\usepackage{nordwind}
\title{Lorem Ipsum}
\author{Erika Muster}
\date{21 September 2026}
\begin{document}
\maketitle
""" + "\n".join(
"\\%s{%s}\n\n%s\n" % (("section", "subsection", "subsubsection")[level],
head, text)
for level, (head, text) in enumerate(TWIN_BODY)) + r"""
\end{document}
"""
def bands(pdf):
"""The rows of ink of the first page, grouped into blocks, in points."""
page = Page(pdf, 1)
factor = 72.0 / page.dpi
grouped, current = [], []
for row in page.ink_rows():
if current and row - current[-1] > 1:
grouped.append((current[0] * factor, current[-1] * factor))
current = []
current.append(row)
if current:
grouped.append((current[0] * factor, current[-1] * factor))
return grouped
# A picture out of Markdown: the same rules as one written in LaTeX, and the
# converter is what says which of them is a raster of pixels.
PICTURE_MD = """---
title: Lorem Ipsum
author: Erika Muster
---
## Consetetur
![Lorem ipsum dolor sit amet](example-image-16x9.pdf)
## Dolor Sit
![Consetetur sadipscing elitr](example-image-a.png)
"""
def pictures(check):
"""A picture out of Markdown takes the size of the family.
The converter hands every picture to pandoc's own bound, and that is where
the rules of the template hang: the lying picture on the first page takes
the whole line, the raster one on the second is the one the converter marks
as pixels, and both stand centred.
"""
path = support.write("picture.md", PICTURE_MD)
result = support.run(["python3", CONVERTER, "--identity", "nordwind",
"--out-dir", OUT, path])
pdf = os.path.join(OUT, "picture.pdf")
check.that(os.path.exists(pdf),
f"the document with the pictures did not build:\n"
f"{result.stdout[-1000:]}")
if not os.path.exists(pdf):
return
for number, name in ((1, "lying"), (2, "raster")):
page = Page(pdf, number)
factor = 72.0 / page.dpi
rows = page.ink_rows()
check.that(rows, f"the {name} picture is not on page {number}")
if not rows:
continue
grouped, current = [], []
for row in rows:
if current and row - current[-1] > 1:
grouped.append(current)
current = []
current.append(row)
grouped.append(current)
tallest = max(grouped, key=lambda band: band[-1] - band[0])
columns = page.ink_columns(tallest[0], tallest[-1] + 1)
width = (columns[-1] - columns[0] + 1) * factor
left = columns[0] * factor
right = page.width * factor - columns[-1] * factor
check.that(width <= 545,
f"the {name} picture is {width:.1f} points wide, more than "
"the 540.6 of the line")
check.that(abs(left - right) < 4,
f"the {name} picture stands {left:.1f} points from the left "
f"edge and {right:.1f} from the right, so it is not centred")
def twins(check):
"""A document out of Markdown is the document out of LaTeX.
Both roads end in the same template, so the same content has to stand in
the same places: same distances under the title block, same three lines
over every heading, same line for every paragraph.
"""
md = support.write("twin.md", TWIN_MD)
result = support.run(["python3", CONVERTER, "--identity", "nordwind",
"--out-dir", OUT, md])
converted = os.path.join(OUT, "twin.pdf")
check.that(os.path.exists(converted),
f"the twin out of Markdown did not build:\n"
f"{result.stdout[-1000:]}")
written, _ = support.build(support.write("twin.tex", TWIN_TEX), EXAMPLES)
if not os.path.exists(converted):
return
out_of_md, out_of_tex = bands(converted), bands(written)
check.that(len(out_of_md) == len(out_of_tex),
f"the page out of Markdown carries {len(out_of_md)} blocks of "
f"ink and the one out of LaTeX {len(out_of_tex)}")
if len(out_of_md) != len(out_of_tex):
return
worst = max(abs(a[0] - b[0]) for a, b in zip(out_of_md, out_of_tex))
check.that(worst < 1,
f"a block of the page stands {worst:.1f} points lower out of "
"Markdown than out of LaTeX")
def missing(check):
"""A document that does not exist ends in a message and never in a PDF."""
result = support.run(["python3", CONVERTER, "--identity",
"nordwind", "--out-dir", OUT,
os.path.join(OUT, "gibtesnicht.md")])
check.that(result.returncode != 0,
"the converter reported success for a file that does not exist")
check.that("gibtesnicht.md" in result.stdout,
"the message does not name the file that is missing")
check.that(not os.path.exists(os.path.join(OUT, "gibtesnicht.pdf")),
"the converter produced a PDF for a file that does not exist")
def main():
check = Check("md2pdf")
runnable(check)
pdf = convert(check)
if os.path.exists(pdf):
measured(check)
inside(check, pdf)
content(check, pdf)
formula(check)
pictures(check)
twins(check)
missing(check)
return check.done()
if __name__ == "__main__":
sys.exit(main())
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""The values the family decided, against the defaults of this suite.
The suite came out of `pacta.sty`, and the proportions of its logo were decided
there and confirmed for every company of the family. A derivation that replaces
one of those numbers has to REPRODUCE it; where it does not, it is a new value,
and nobody decided it.
That is not an abstract risk. On 2026-09-21 this suite carried four derivations
in place of those numbers for half a day — the logo of the page head measured
off the running head instead of 2em, the one of the title block as three times
that instead of 6em, the name beside the logo fitted to the ink of the drawing
instead of 0.8 of its height, the gap in ems of the name instead of 0.1 of that
height — and every one of them came out at another size than in the two companies
beside it. It was visible on the first page of the first document.
The origin is read where LaTeX finds it. Where it is not installed, this says so
and passes: a company that has no Pacta beside it cannot compare, and a test that
fails for a missing neighbour says nothing about this suite.
"""
import os
import re
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import support
from support import Check
SUITE = os.path.join(support.SUITE, "business-suite.sty")
IDENTITY = os.path.join(support.SUITE, "business-identity.sty")
# What is compared, and where each value stands in the two files.
HEIGHTS = (
("logoheight", r"\\newcommand\{\\pacta@logoheight\}\{([^}]*)\}",
r"\\newcommand\{\\business@logoheight\}\{([^}]*)\}",
"the logo of the page head"),
("titlelogoheight", r"\\newcommand\{\\pacta@titlelogoheight\}\{([^}]*)\}",
r"\\newcommand\{\\business@titlelogoheight\}\{([^}]*)\}",
"the logo of the title block"),
)
# The three proportions of the lockup, as the origin writes them: the name at
# 0.8 of the height, the gap beside it at 0.1, the gap over the address at 0.1.
FACTORS = (
("namefactor", 0.8, r"\\newcommand\{\\business@namefactor\}\{([0-9.]+)\}",
"the size of the name beside the logo"),
("namegap", 0.1, r"\\newcommand\{\\business@namegap\}\{([0-9.]+)\}",
"the gap between the logo and the name"),
("addressgap", 0.1, r"\\newcommand\{\\business@addressgap\}\{([0-9.]+)\}",
"the gap between the lockup and the address line"),
)
def read(path):
with open(path, encoding="utf-8") as handle:
return handle.read()
def first(pattern, text):
found = re.search(pattern, text)
return found.group(1) if found else None
def main():
check = Check("origin")
ours = read(SUITE) + read(IDENTITY)
# The factors of the family stand here as numbers, because they are what
# the origin writes into its own `\fpeval` calls and cannot be read out of
# it without parsing TeX. The heights are read out of the file itself.
for name, value, pattern, what in FACTORS:
mine = first(pattern, ours)
check.that(mine is not None, f"{name} is not declared in this suite")
check.that(mine is not None and abs(float(mine) - value) < 0.0001,
f"{what}: this suite says {mine}, the family {value}")
origin = support.run(["kpsewhich", "pacta.sty"]).stdout.strip()
if not origin or not os.path.exists(origin):
print(" pacta.sty is not installed; the heights were not compared")
return check.done()
theirs = read(origin)
for name, there, here, what in HEIGHTS:
want = first(there, theirs)
mine = first(here, ours)
check.that(want is not None, f"{name} was not found in {origin}")
check.that(mine is not None, f"{name} is not declared in this suite")
check.that(want is not None and mine is not None and want == mine,
f"{what}: this suite says {mine}, the origin {want}")
return check.done()
if __name__ == "__main__":
sys.exit(main())
+195
View File
@@ -0,0 +1,195 @@
#!/usr/bin/env python3
"""What every test of this suite needs: build a document, read its page.
A test of a template measures the PAGE and not the source: a command can carry
the right name and put the wrong thing on the paper, and a package can define
every colour a company asks for and never use one of them.
"""
import os
import re
import shutil
import subprocess
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
BUILD = os.path.join(ROOT, "tests", "build")
SUITE = os.path.join(ROOT, "texmf", "tex", "latex", "business-suite")
def run(command, cwd=ROOT, environment=None):
return subprocess.run(command, cwd=cwd,
env=dict(os.environ, **(environment or {})),
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
encoding="utf-8", errors="replace")
def searchpath(*directories):
return os.pathsep.join([directory + "//" for directory in
(ROOT,) + directories] + [""])
def build(source, directory=None):
"""One document, built the way the npm script builds it.
The source may be a path in the repository or a file written for the test;
what comes back is the path of the PDF and the path of the log.
"""
directory = directory or os.path.dirname(os.path.abspath(source))
stem = os.path.splitext(os.path.basename(source))[0]
out = os.path.join(BUILD, stem)
os.makedirs(out, exist_ok=True)
# `-g` forces the run even where latexmk thinks the result is up to date.
# Without it a test reads the log of the LAST build that really happened,
# and a warning that belongs to a state the working tree has left stands in
# it — measured on 2026-09-21, where two fixtures reported a rerun that the
# current source no longer asks for.
result = run(["latexmk", "-xelatex", "-interaction=nonstopmode",
"-file-line-error", "-emulate-aux-dir", "-halt-on-error", "-g",
"-auxdir=" + out, "-outdir=" + out,
os.path.abspath(source)],
cwd=directory,
environment={"TEXINPUTS": searchpath(directory, out)})
pdf = os.path.join(out, stem + ".pdf")
log = os.path.join(out, stem + ".log")
if not os.path.exists(pdf):
raise AssertionError(f"{source} did not build:\n{result.stdout[-2000:]}")
return pdf, log
def write(name, text):
"""A fixture document, in the build directory beside its result.
What was built from the last version of this fixture goes with it. latexmk
decides from its own database whether a run is needed, and for a fixture
that is rewritten under the same name it decided wrong: the test then reads
a PDF built from a source that no longer exists — measured on 2026-09-21,
where a renamed fixture was still measured against the text of the run
before it.
"""
os.makedirs(BUILD, exist_ok=True)
shutil.rmtree(os.path.join(BUILD, os.path.splitext(name)[0]),
ignore_errors=True)
path = os.path.join(BUILD, name)
with open(path, "w", encoding="utf-8") as handle:
handle.write(text)
return path
class Page:
"""One rendered page, as pixels, with the questions a layout test asks."""
def __init__(self, pdf, page=1, dpi=150):
stem = os.path.join(BUILD, "page")
run(["pdftoppm", "-r", str(dpi), "-f", str(page), "-l", str(page),
"-gray", pdf, stem])
name = f"{stem}-{page}.pgm"
if not os.path.exists(name):
name = f"{stem}-{page:02d}.pgm"
with open(name, "rb") as handle:
data = handle.read()
fields, offset = [], 0
while len(fields) < 4:
end = data.index(b"\n", offset)
line = data[offset:end]
offset = end + 1
if not line.startswith(b"#"):
fields += line.split()
self.width, self.height = int(fields[1]), int(fields[2])
self.pixels = data[offset:]
self.dpi = dpi
# What counts as ink. The furniture of this page is QUIET — a page number at
# 55 percent of a dark blue comes out around 160 in grey — so a threshold
# that only sees black misses exactly the things a layout test looks for:
# measured on 2026-09-21, when the example company changed its palette and the
# page number stopped existing for the test while standing on the page.
INK = 200
def dark(self, x, y):
return self.pixels[y * self.width + x] < self.INK
def ink_rows(self):
"""Every row of the page that carries ink, from the top."""
return [y for y in range(self.height)
if any(self.dark(x, y) for x in range(self.width))]
def ink_columns(self, top=0, bottom=None):
bottom = self.height if bottom is None else bottom
return [x for x in range(self.width)
if any(self.dark(x, y) for y in range(top, bottom))]
def box(self, top=0, bottom=None):
"""The rectangle the ink of a band of rows stands in, in points."""
bottom = self.height if bottom is None else bottom
columns = self.ink_columns(top, bottom)
rows = [y for y in range(top, bottom)
if any(self.dark(x, y) for x in range(self.width))]
if not columns or not rows:
return None
factor = 72.0 / self.dpi
return (columns[0] * factor, rows[0] * factor,
columns[-1] * factor, rows[-1] * factor)
def fonts(pdf):
"""Which faces the finished PDF really carries."""
result = run(["pdffonts", pdf])
found = []
for line in result.stdout.splitlines()[2:]:
if line.strip():
found.append(line.split()[0].split("+")[-1])
return found
def pages(pdf):
result = run(["pdfinfo", pdf])
found = re.search(r"^Pages:\s+(\d+)", result.stdout, re.MULTILINE)
return int(found.group(1)) if found else 0
def text(pdf):
result = run(["pdftotext", "-layout", pdf, "-"])
return result.stdout
def words(pdf, page=1):
"""Every word of one page with the box it stands in, in points.
What comes back is a list of (word, left, top, right, bottom). The raster
cannot answer everything a layout test asks: two lines of text that a logo
stands beside are one band of ink, because the logo bridges the gap between
them. The text layer carries each word on its own, with the box the type
really occupies, so lines that touch in the picture stay two lines here.
"""
result = run(["pdftotext", "-bbox", "-f", str(page), "-l", str(page),
pdf, "-"])
found = []
for line in result.stdout.splitlines():
box = re.search(r'<word xMin="([\d.]+)" yMin="([\d.]+)" '
r'xMax="([\d.]+)" yMax="([\d.]+)">(.*)</word>', line)
if box:
found.append((box.group(5),) + tuple(float(box.group(index))
for index in (1, 2, 3, 4)))
return found
class Check:
"""The count and the first failure, printed the way a suite prints it."""
def __init__(self, name):
self.name = name
self.green = 0
self.failures = []
def that(self, condition, message):
if condition:
self.green += 1
else:
self.failures.append(message)
def done(self):
for failure in self.failures:
print(f" {failure}", file=sys.stderr)
total = self.green + len(self.failures)
print(f"{self.name}: {self.green}/{total}")
return 1 if self.failures else 0