. "$BST_LIB" # BST-TIMEOUT: 1800 # TEN python3 starts, and they are the whole cost. Every # # assertion below either reads the filesystem/dpkg with # # shell builtins or greps the output of one of those ten. # # # # Three are the module: a bare `import', a second one whose # # STDERR is the assertion (`import-quiet'), and the probe, # # which takes ONE start for ninety-odd measurements. The # # other seven are the SHIPPED SCRIPT, and they cannot be # # folded into the probe -- the point of them is that # # /usr/bin/smartypants is a separate executable reached # # through its `#!/usr/bin/env python3' line, so running its # # code in-process would assert the module a second time and # # the script not at all. # # # # The work inside each start is a handful of `re.sub' calls # # over strings under 100 bytes -- microseconds beside an # # interpreter start on this arm, which is tens of seconds # # before the first line of the program runs. # # # # 1800 is 180s per start. 99-python3-wcwidth budgets 1200 # # for three, i.e. 400s each; this is tighter per start and # # still three times the ~60s a python3 start has measured at # # on this arm, because the start dominates and nothing here # # is quadratic in anything. # # # # A suite-timeout red is a batch-wide cost, so if the ten # # starts prove slower than that on target the RIGHT fix is # # this number, not fewer cases. # # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify, and the measured elapsed replaces # # this note in the test plan rather than in this comment, # # which would cost an image build to keep the shipped suite # # byte-identical. # # python3-smartypants 2.0.0 (poky/meta # recipes-devtools/python/python3-smartypants_2.0.0.bb, `inherit pypi # setuptools3', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES, AND THE ONE THING ITS DESCRIPTION GETS WRONG. # ============================================================================ # # `smartypants(text)' rewrites ASCII punctuation into its typographic form: # straight quotes curl, `--' becomes a dash, `...' becomes an ellipsis. It is # John Gruber's SmartyPants, ported to Python. # # But it does NOT return curly characters. What it returns by DEFAULT is NUMERIC # CHARACTER REFERENCES -- `“', not U+201C -- which are curly only once a # browser has rendered them. A caller that assumed characters gets entities, and # that is the single most likely way to misuse this package, so `mode-*' below # asserts all four output forms separately: the default numeric references, # `Attr.u' for real Unicode characters, `Attr.h' for HTML named entities, and # `Attr.s' -- stupefy -- which converts them back to ASCII. # # `round-trip' is the case that makes the pair mean something: educate, then # stupefy, and land back on the exact bytes you started from. # # ============================================================================ # THE FLAGS ARE THE CONTRACT, AND THREE OF THEM ARE COMPOSITE. # ============================================================================ # # What gets converted is selected by an integer of OR-ed bits, `Attr'. The bits # are not an implementation detail: the shipped `smartypants' script builds its # mask by looking flag names up on `Attr' from its `-a' argument, so `-a qw' is # defined by these numbers and a release that renumbered them would change what # every CLI caller means without changing anything a behavioural case could see. # `attr-*' pins all eighteen. # # Three are composite -- `B' implies `b', `D' and `i' each imply `d' -- and the # dispatch inside `smartypants()' compares `attr & mask_d' against `d', `D' and # `i' by EQUALITY. That is what makes those implications load-bearing rather # than decorative, and it has a consequence worth asserting on its own: # # `only-B' -- `Attr.B' does NOT give you the double-backtick conversion, # even though B is defined as `1 << 2 | b'. `do_backticks' comes out equal to # `B', so the `== Attr.b' branch that calls `convert_backticks' never runs and # only `convert_single_backticks' does; ``` ``b'' ``` comes back as two SINGLE # quote entities rather than one double. That is upstream 2.0.0's behaviour, # it is not this platform's, and it is recorded here so a reader who worked # the flag arithmetic out from the definitions is told what actually happens. # # ============================================================================ # THREE DASH MAPPINGS, ONE OF WHICH IS BACKWARDS FROM ITS OWN NAME. # ============================================================================ # # `convert_dashes' maps `--' to an em dash and leaves `---' as an em dash # followed by a stray hyphen -- `dashes-set1' asserts that literal, because it # is the default behaviour and it surprises everyone. # # `convert_dashes_oldschool' maps `--' to an EN dash and `---' to an EM dash; # `convert_dashes_oldschool_inverted' maps them the other way round. Upstream's # own comment on the first pair of lines reads "(yes, backwards)": the source # labels the `---' substitution `em' and the `--' substitution `en', which is # what the function does but the reverse of what the variable names next to # them say. `fn-dashes-old*' assert what the functions DO, so a reader who # trusted either the name or the inline label is corrected here rather than # after shipping. # # ============================================================================ # THE TWO EXEMPTIONS ARE REACHED BY DIFFERENT CODE, SO THEY ARE ASSERTED APART. # ============================================================================ # # Text inside an HTML TAG is never a text token at all -- the tokenizer emits # it as a tag and `smartypants()' appends it verbatim -- so `skip-tag' is a # statement about the tokenizer. # # Text inside a SKIPPED ELEMENT (`pre', `code', `kbd', `script', and four more) # IS a text token; it is handed over with `in_pre' set and every conversion is # stepped over. That is a statement about the main loop, and about the STACK it # keeps: `skip-nested' puts a `' inside a `
' and asserts that the
# inner close does not re-enable conversion for the rest of the outer element,
# which a boolean flag rather than a stack would get wrong.
#
# `skip-control' is the case that keeps the other nine honest: `' is NOT on
# the list, and its content IS converted. Without it every skip case above would
# pass on an implementation that simply skipped all markup.
#
# `skip-custom' asserts the list is module state a caller may replace, because
# that is what the shipped script's `-s' does -- and `cli-skip' asserts the
# script actually does it.
#
# ============================================================================
# WHY IT PORTS AT ALL: PURE PYTHON, ONE FILE, python3-core AND NOTHING ELSE.
# ============================================================================
#
# This image's python3 is ONE STATIC BINARY with an EMPTY lib-dynload and no
# dynamic loader (BRINTOS-1476; 99-python3 asserts both directly), so a module
# carrying a compiled extension could not be imported here -- not "would be
# slow", could not be imported.
#
# smartypants ships no C: setup.py declares `py_modules' and `scripts' and no
# ext_modules at all, and the module imports `re' and nothing else. MEASURED off
# the built python3-smartypants_2.0.0-r0_wasm32.deb: the control stanza reads
# `Depends: python3-core' and nothing else, so this port waits on no other
# package. `pkg-depends-core' is what would say so if a release grew a
# third-party requirement.
#
# It is a MODULE, NOT A PACKAGE -- `py_modules=['smartypants']', so ONE FILE
# lands in site-packages rather than a directory. That is the opposite shape
# from 99-python3-wcwidth's, and `is-module' asserts it directly, because a
# release that moved to a package would still import and still pass every
# behavioural case in this file.
#
# ============================================================================
# THE SHIPPED SCRIPT, AND THE oe-core PATCH IT DEPENDS ON.
# ============================================================================
#
# The package installs /usr/bin/smartypants, a plain Python script -- not a
# console_scripts entry point, so there is no generated launcher and the shebang
# line IS the dispatch. Upstream's shebang is `#!/usr/bin/env python', which
# names an interpreter this image does not have; the recipe's one patch,
# 0001-Change-hash-bang-to-python3.patch, rewrites it to `python3' in setup.py,
# in the script and in the module. `script-shebang' asserts the patched line
# verbatim, because a rebuild that lost the patch would ship a file that is
# still executable, still passes every packaging case, and cannot run.
#
# It also means the script's execution depends on /usr/bin/env resolving
# `python3' off PATH -- a second image package's job, not this one's -- so
# `env-present' is stated separately from `cli-version'. When the CLI cases red
# together, that pair is what says whether the cause is this package.
#
# ============================================================================
# ONE SHAPE THIS SUITE DELIBERATELY DOES NOT ASSERT.
# ============================================================================
#
# There is no ptest question here. Unlike python3-wcwidth, this recipe does not
# `inherit ptest' at all, so no -ptest package could be produced whatever
# DISTRO_FEATURES said, and upstream's `setup.py test' discovers a tests/
# directory the sdist does not ship. What upstream's doctests cover -- every
# example in the module's docstrings -- is reproduced case by case in the probe
# instead, which is why `fn-quotes', `fn-backticks', `fn-dashes' and
# `fn-escapes' use upstream's own documented example strings verbatim.
#
# ============================================================================
# WHERE THE EXPECTED VALUES CAME FROM.
# ============================================================================
#
# Every literal below was measured on x86-64 against the bytes unpacked from the
# built python3-smartypants_2.0.0-r0_wasm32.deb -- not against the sdist, so the
# recipe's patch is inside what was measured. The control interpreter is CPython
# 3.10 and the image ships 3.12; the package is `re' over ASCII strings with no
# version-dependent construct in it, and the one place that gap could show is
# `import-quiet', which is called out where it is asserted. The on-target result
# is not yet measured -- the batch verify is this port's first run on
# wasm32-hwjs.

P=python3-smartypants

# THE CANARY, which asserts nothing about python3-smartypants.
#
# A suite's FIRST BST result line is lost between the guest's printf and the
# host's read -- per-suite, deterministic, and NOT a defect in the host tool
# (BRINTOS-1149 established that and filed it as linux-in-the-browser#177). The
# harness computes its pass count as total-minus-failures, so THE DROPPED LINE
# IS COUNTED AS A PASS. A nothing-case therefore stands in that position and the
# eaten line costs nothing. It sits ahead of the `require' guards deliberately,
# because a `require' that skips would emit its skip as the first line and be
# eaten too.
#
# Remove this case when linux-in-the-browser#177 is fixed.
bst_ok $P/suite-canary

require python3 $P/python3-present

SITE=/usr/lib/python3.12/site-packages
MOD=$SITE/smartypants.py
DI=$SITE/smartypants-2.0.0.dist-info
CLI=/usr/bin/smartypants
PKGLIST=/var/lib/dpkg/info/python3-smartypants.list
PROBE=$BST_DATA/python3-smartypants-probe.py
tmp=${TMPDIR:-/tmp}
W=$tmp/bst-smartypants.$$
mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; }

# ------------------------------------------------------------- the payload ---
# Where the package has to land. `import smartypants' further down is the
# functional statement; these are the ones that say WHICH copy answered, so a
# package that imported out of some other path on sys.path is distinguishable
# from one that imported out of what this image installed.
check $P/site-packages-dir test -d "$SITE"
check $P/module-file       test -s "$MOD"
# ONE FILE, NOT A DIRECTORY. See the header: `py_modules=['smartypants']' is the
# opposite shape from a package, and this is the case that fails a release which
# moved to one -- loudly, here, rather than nowhere.
check $P/no-package-dir    test ! -d "$SITE/smartypants"
# The precompiled bytecode, which is what `import-quiet' below turns on.
check $P/pycache test -s "$SITE/__pycache__/smartypants.cpython-312.pyc"

# THE SHIPPED SCRIPT, as a file. `cli-*' below run it; these say it is there and
# that it is executable, so a red in the CLI group has somewhere to start.
check $P/script-present    test -f "$CLI"
check $P/script-executable test -x "$CLI"
# THE oe-core PATCH, ASSERTED VERBATIM. See the header: upstream's shebang names
# `python', which this image does not have, and the recipe's single patch is
# what makes the script runnable at all. A rebuild that lost it ships a file
# that passes every case above and cannot run.
check_eq $P/script-shebang "#!/usr/bin/env python3" \
  "$(head -n 1 "$CLI" 2>/dev/null)"
# ...and the interpreter that shebang goes through. This is another package's
# job, not this one's; it is stated separately so that when the CLI group reds
# together, this case says whether the cause is here.
check $P/env-present test -x /usr/bin/env

# NO COMPILED EXTENSION ANYWHERE THE PACKAGE PUT ANYTHING. See the header: on
# this image a .so could not be loaded at all, so this is the case that fails a
# version bump which grew one -- at install rather than at `import'.
#
# The list is copied to tmpfs first and never redirected from the rootfs: a
# top-level stdin redirect on the virtiofs root makes dup2(2) return 513 and ash
# EXITS, ending the suite silently (brintos/linux#73).
cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf"
check $P/pkg-list-present test -s "$W/pkgf"
check_eq $P/pkg-no-so "" \
  "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)"

# EVERY REGULAR FILE THE PACKAGE SHIPS IS PYTHON, ITS METADATA, OR THE ONE
# SCRIPT. The case above names the failure that is expected; this one catches
# the shape nobody predicted -- a binary, a .pyd, a stray data file -- without
# having to enumerate what it might be. The script is the single exemption, and
# it is exempted BY NAME rather than by living in ${bindir}, so a package that
# grew a second executable is reported rather than waved through.
#
# A dpkg .list names DIRECTORIES too, and without a trailing slash, so a
# name-only filter would report every directory in the package as an unexpected
# shape; each entry is therefore tested for being a regular file first.
_bad=""
while IFS= read -r _f; do
  [ -f "$_f" ] || continue
  case "$_f" in
    "$CLI")             continue ;;
    *.py|*.pyc)         continue ;;
    */__pycache__/*)    continue ;;
    */*.dist-info/*)    continue ;;
  esac
  _bad="$_bad $_f"
done < "$W/pkgf"
check_eq $P/pkg-only-python "" "$_bad"

# ----------------------------------------------------------- the dist-info ---
# The installed metadata. This module carries its own `__version__', so the
# dist-info is the SECOND of two independent statements of the version rather
# than the only one, and `version-agrees' below is what compares them.
check $P/dist-info-dir      test -d "$DI"
check $P/dist-info-metadata test -f "$DI/METADATA"
check_out $P/dist-info-version "Version: 2.0.0" cat "$DI/METADATA"
# top_level.txt names the ONE importable this package installs. It is what an
# `import smartypants' has to match, and a wheel that renamed the module would
# change it here first.
check_out $P/dist-info-top-level "smartypants" cat "$DI/top_level.txt"
# PURE PYTHON, AS THE WHEEL ITSELF DECLARES IT. `Root-Is-Purelib: true' is the
# packaging-side statement of the header's claim -- no platform tag, no
# extension module, nothing that could need the absent dynamic loader.
check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL"
# The upstream licence text ships beside the metadata, which is where this
# recipe's LIC_FILES_CHKSUM points.
check $P/dist-info-copying test -s "$DI/COPYING"

# ------------------------------------------------------------- the package ---
# dpkg KNOWS the package. Every case above would look identical if some other
# package had installed the same files, so this is the one that says
# python3-smartypants is a real package this image installed rather than an
# IMAGE_INSTALL line that resolved to nothing.
#
# The stanza is read from a tmpfs copy for the same reason as the file list, and
# the anchor is exact -- an unanchored match would also take the -dev or -dbg
# stanza of the same family.
sed -n '/^Package: python3-smartypants$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null
check $P/pkg-known test -s "$W/st"
check_out $P/pkg-installed "Status: install ok installed" cat "$W/st"

# THE RUNTIME DEPENDENCY THE RECIPE DECLARES, as dpkg recorded it -- and here it
# is the WHOLE of it. smartypants imports `re' and nothing else, so the built
# package depends on python3-core alone; that is why this port waits on no other
# package, and this case is what would say so if a release grew a third-party
# requirement.
grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep"
check $P/pkg-depends-line test -s "$W/dep"
check_out $P/pkg-depends-core "python3-core" cat "$W/dep"

# ---------------------------------------------------------- the interpreter ---
# THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If smartypants cannot be imported
# then every probe case below reds at once and each names a different symptom;
# this case is what says the cause is the import. It is also the cheapest
# possible statement of the deliverable.
check $P/import python3 -c 'import smartypants'

# ...AND THE IMPORT SAYS NOTHING WHILE DOING IT. This is not a style assertion,
# it is a statement about the shipped bytecode. smartypants.py contains two
# `re.match("\S", ...)' calls whose `\S' is an invalid escape sequence in a
# non-raw string literal; CPython 3.12 raises that from DeprecationWarning to
# SyntaxWarning, which is shown by DEFAULT. It is not printed here because the
# package ships a precompiled .pyc and OE writes it with CHECKED-HASH
# invalidation (measured off the built .deb: pyc header flags = 3, i.e.
# hash-based with check_source set), so the source is hashed and never
# recompiled. A red here means the shipped .pyc was REJECTED -- a stale hash, a
# python3 minor bump that changed the magic, or a missing __pycache__ -- and the
# module is being compiled from source on every import.
#
# PREDICTED, NOT MEASURED: the control interpreter is 3.10, where the same
# escape is only a DeprecationWarning and is silent by default, so this is the
# one case in this file whose expectation was reasoned from the 3.12 change and
# the measured pyc flags rather than observed. The batch verify is what settles
# it.
check_eq $P/import-quiet "" \
  "$(python3 -c 'import smartypants' 2>&1)"

# ---------------------------------------------------------------- the probe ---
if [ ! -f "$PROBE" ]; then
  bst_notok $P/probe-present "$PROBE did not ship"
  rm -rf "$W"
  exit 0
fi
bst_ok $P/probe-present

# ONE interpreter start for everything that follows. bst_probe bounds it and
# keeps BOTH streams: a probe that dies early otherwise leaves every case below
# reporting got:'' and naming a symptom each, with the actual traceback nowhere.
# The bound is generous because a python3 start on this arm is tens of seconds
# before any work begins.
bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE"

OUT="$W/probe"
# Read a single key. The probe emits each key at most once, so head -n 1 is
# defensive rather than load-bearing; the anchors keep `only_b' distinguishable
# from `only_B' -- which differ by case alone and are the pair this suite most
# needs to keep apart.
pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; }

# THE EVIDENCE PLANE. If the probe emitted nothing, every case below is reading
# an absent file: each would red with got:'' and the set would read exactly like
# a broken library, when what actually happened is that no measurement was
# taken. The word `unscorable' is PARSED -- brintos_local_verify re-measures a
# suite that prints it on a fresh guest rather than trusting a shared boot's
# numbers (BRINTOS-1644) -- so it goes in this structural check, not a comment.
#
# The cases below still print their reds: withdraw, do not hide. This case is
# what says what those reds measure.
if [ -s "$OUT" ]; then
  bst_ok $P/probe-output
else
  bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable"
fi

# ...and it ran to the END. Every key is emitted in file order, so a probe that
# died two thirds of the way through would leave the earlier cases green and the
# later ones reading got:'' -- which looks like a partial library rather than a
# partial run. This is the case that tells those apart.
check_eq $P/probe-complete "yes" "$(pv probe_done)"
check_eq $P/probe-count    "92"  "$(pv emitted_count)"

# The module answered out of the file this image installed, not from a stray
# copy on sys.path. This is what makes the payload cases above and the behaviour
# cases below statements about the SAME smartypants.
check_eq $P/module-answered "$MOD" "$(pv module_file)"
# ...and it is a MODULE. See the header: a release that moved to a package
# directory would still import and still pass every behavioural case below.
check_eq $P/is-module "yes" "$(pv is_module)"

# THE VERSION, FROM BOTH PLACES, AND THEN AGAINST EACH OTHER. The module's own
# `__version__' and the installed dist-info read through importlib.metadata are
# independent statements; `version-agrees' is the one that catches a module that
# landed beside a dist-info describing some other release, which either case
# alone would pass.
check_eq $P/version-attr   "2.0.0" "$(pv version_attr)"
check_eq $P/dist-version   "2.0.0" "$(pv dist_version)"
check_eq $P/version-agrees "$(pv version_attr)" "$(pv dist_version)"
check_eq $P/license-attr   "BSD License" "$(pv license_attr)"
# The module's own one-line description, which is what reaches the dist-info's
# `Summary'. The .deb's Description field carries the RECIPE's SUMMARY instead
# and the two differ in wording; both are upstream's, neither is wrong, and this
# case pins the one the module states about itself.
check_eq $P/description "Python with the SmartyPants" "$(pv description)"

# ---------------------------------------------------------------- the flags ---
# ALL EIGHTEEN ATTRIBUTE BITS. See the header: these numbers are what `-a qw'
# means, so a renumbering changes every CLI caller's behaviour while every
# behavioural case in this file still passes.
check_eq $P/attr-q  "1"   "$(pv attr_q)"
check_eq $P/attr-b  "2"   "$(pv attr_b)"
check_eq $P/attr-B  "6"   "$(pv attr_B)"
check_eq $P/attr-d  "8"   "$(pv attr_d)"
check_eq $P/attr-D  "24"  "$(pv attr_D)"
check_eq $P/attr-i  "40"  "$(pv attr_i)"
check_eq $P/attr-e  "64"  "$(pv attr_e)"
check_eq $P/attr-w  "128" "$(pv attr_w)"
check_eq $P/attr-u  "256" "$(pv attr_u)"
check_eq $P/attr-h  "512" "$(pv attr_h)"
check_eq $P/attr-s  "768" "$(pv attr_s)"
# The masks the dispatch actually tests against. `mask_b' being 6 rather than
# `b|B' arithmetic a reader might expect is the mechanism behind `only-B'.
check_eq $P/attr-mask-b "6"   "$(pv attr_mask_b)"
check_eq $P/attr-mask-d "56"  "$(pv attr_mask_d)"
check_eq $P/attr-mask-o "768" "$(pv attr_mask_o)"
# The four named sets, which are what `-a 0'..`-a 3' resolve to.
check_eq $P/attr-set0 "0"   "$(pv attr_set0)"
check_eq $P/attr-set1 "75"  "$(pv attr_set1)"
check_eq $P/attr-set2 "91"  "$(pv attr_set2)"
check_eq $P/attr-set3 "107" "$(pv attr_set3)"
# The default a no-argument call resolves to, read through the `Attr.default'
# property and off the module global it fronts. They must agree and they must
# be set1.
check_eq $P/attr-default   "75" "$(pv attr_default)"
check_eq $P/default-global "75" "$(pv default_global)"

# The public API, by name. Each of these is documented and each is reached by at
# least one behavioural case below; this group is what says a red further down
# is about behaviour rather than about a function that is not there.
for _n in smartypants convert_quotes convert_backticks convert_single_backticks \
          convert_dashes convert_dashes_oldschool \
          convert_dashes_oldschool_inverted convert_ellipses convert_entities \
          process_escapes; do
  check_eq $P/api-$(echo "$_n" | tr _ -) "yes" "$(pv has_$_n)"
done

# The default skip list, in order. It is module state a caller may replace, so
# its default membership is part of the contract rather than an internal.
check_eq $P/tags-to-skip "pre,samp,code,tt,kbd,script,style,math" \
  "$(pv tags_to_skip)"
check_eq $P/tags-to-skip-count "8" "$(pv tags_to_skip_count)"

# -------------------------------------------------------------- the fixture ---
# ONE LINE CARRYING EVERY TRANSFORMATION, converted under the default attribute
# and asserted against a literal. `don't' is the case a naive "first quote
# opens, second closes" implementation gets wrong: the apostrophe is INSIDE a
# word and must close, while the `'don' before it must open.
check_eq $P/fixture-default \
  "“He said, ‘don’t’ — really…” and more" \
  "$(pv fixture_default)"
# The default IS set1, stated as an equality between two conversions rather than
# as two identical literals -- so a release that moved the default says so here
# instead of everywhere at once.
check_eq $P/fixture-set1 "$(pv fixture_default)" "$(pv fixture_set1)"
# set0 IS the identity. Without it, every case above would pass on an
# implementation that ignored `attr' entirely and always did everything.
check_eq $P/fixture-set0 "$(pv fixture)" "$(pv fixture_set0)"

# THE THREE DASH SETS OVER ONE STRING. See the header: `dashes-set1' is the
# surprising one -- the default maps `--' to an em dash and leaves the third
# hyphen of `---' sitting after it.
check_eq $P/dashes-set1 "a — b —- c" "$(pv dashes_set1)"
check_eq $P/dashes-set2 "a – b — c" "$(pv dashes_set2)"
check_eq $P/dashes-set3 "a — b – c" "$(pv dashes_set3)"

# ------------------------------------------------------- one flag at a time ---
# EACH FLAG ALONE, over a fixture carrying all four constructs. A suite that
# only ran the sets could not tell a flag that is wired up from one whose effect
# a neighbour is producing.
check_eq $P/only-q "“q” ‘r’ -- ... \`\`b’’ \`s’" \
  "$(pv only_q)"
check_eq $P/only-b "\"q\" 'r' -- ... “b” \`s'" "$(pv only_b)"
# THE COMPOSITE FLAG THAT DOES NOT DO WHAT ITS DEFINITION SUGGESTS. See the
# header: `B' is `1 << 2 | b', but `do_backticks' then equals `B', so the branch
# calling `convert_backticks' never runs and ``` `` ``` comes back as two
# SINGLE quote entities. Upstream 2.0.0's behaviour, not this platform's.
check_eq $P/only-B "\"q\" ’r’ -- ... ‘‘b’’ ‘s’" \
  "$(pv only_B)"
check_eq $P/only-d "\"q\" 'r' — ... \`\`b'' \`s'" "$(pv only_d)"
check_eq $P/only-D "\"q\" 'r' – ... \`\`b'' \`s'" "$(pv only_D)"
check_eq $P/only-i "\"q\" 'r' — ... \`\`b'' \`s'" "$(pv only_i)"
check_eq $P/only-e "\"q\" 'r' -- … \`\`b'' \`s'" "$(pv only_e)"

# THE CONVERSION FUNCTIONS DIRECTLY, over upstream's own docstring examples --
# which is what stands in for the doctest suite the sdist does not ship.
check_eq $P/fn-dashes  "a — b —- c" "$(pv fn_dashes)"
# ...and the two whose names are backwards from what they do. See the header.
check_eq $P/fn-dashes-old     "a – b — c" "$(pv fn_dashes_old)"
check_eq $P/fn-dashes-old-inv "a — b – c" "$(pv fn_dashes_old_inv)"
# BOTH ellipsis forms: `...' and the spaced `. . .'.
check_eq $P/fn-ellipses "Huh…? And … so" "$(pv fn_ellipses)"
check_eq $P/fn-quotes   "“Isn’t this fun?”" "$(pv fn_quotes)"
# `convert_backticks' curls the DOUBLE forms and leaves the apostrophe alone;
# `convert_single_backticks' takes every backtick and every apostrophe. The pair
# is what says they are two functions rather than one with a flag.
check_eq $P/fn-backticks "“Isn't this fun?”" "$(pv fn_backticks)"
check_eq $P/fn-single-backticks "‘Isn’t this fun?’" \
  "$(pv fn_single_backticks)"

# THE ESCAPES, which are how a caller forces a DUMB quote through. They run
# BEFORE every other transformation, so `\"' has to come out as `"' rather
# than being curled -- `escaped-in-context' is the case that asserts the
# ordering, and it is the one that would silently stop working if the passes
# were reordered.
check_eq $P/fn-escapes "\ " ' . - `" "$(pv fn_escapes)"
check_eq $P/escaped-in-context "“smarty” "pants"" \
  "$(pv escaped_in_context)"

# --------------------------------------------------------- the output modes ---
# ALL FOUR OUTPUT FORMS. See the header: the default is NUMERIC CHARACTER
# REFERENCES, not curly characters, and that is the package's most likely
# misuse. `entities-*' reach the table directly; `mode-*' reach it through the
# attribute flags, which is the path a caller takes.
check_eq $P/entities-h "“Hello — world…” ‘a’ –" \
  "$(pv entities_h)"
check_eq $P/entities-s "\"Hello -- world...\" 'a' -" "$(pv entities_s)"
# `u' mode is asserted as CODE POINTS rather than as characters. This image's
# guest runs under the C locale and whether CPython coerces stdout to UTF-8
# there is a property of the platform, not of this package: printing U+201C
# would make this case a statement about the encoding. Seven entities, seven
# code points, in the order the table walks them.
check_eq $P/entities-u "U+201C,U+2014,U+2026,U+201D,U+2018,U+2019,U+2013" \
  "$(pv entities_u_cp)"
check_eq $P/mode-h "“He said, ‘don’t’ — really…” and more" \
  "$(pv mode_h)"
check_eq $P/mode-u "U+201C,U+2018,U+2019,U+2019,U+2014,U+2026,U+201D" \
  "$(pv mode_u_cp)"
# STUPEFY, twice. Once over the raw fixture -- where set1|s educates and then
# un-educates in one call, so the answer is the input -- and once as a genuine
# ROUND TRIP over already-curly output. The second is the one that means
# something: educate, stupefy, land back on the exact bytes.
check_eq $P/mode-s     "$(pv fixture)" "$(pv mode_s)"
check_eq $P/round-trip "$(pv fixture)" "$(pv round_trip)"

# `w' converts `"' into a real double quote so the quote pass can educate
# it. Upstream documents that it must be used WITH `q' or `b'; both halves are
# asserted, so the pair says which one does the work rather than leaving `w' to
# be inferred from a single case.
check_eq $P/quot-without-w "a "b" c" "$(pv quot_without_w)"
check_eq $P/quot-with-w    "a “b” c" "$(pv quot_with_w)"

# ----------------------------------------------------- the two exemptions ---
# INSIDE AN HTML TAG. The tag is emitted verbatim -- its `x--y' href and its
# `it's' title are untouched -- while the text between the tags IS converted.
# One case, two statements, and it is the tokenizer that makes it true.
check_eq $P/skip-tag "don’t — x" \
  "$(pv skip_tag)"
# INSIDE A SKIPPED ELEMENT. All eight of the default list, because the list is
# data and a build that shipped seven of them would pass a spot check.
for _t in pre samp code tt kbd script style math; do
  check_eq $P/skip-$_t "<$_t>\"a\" -- b..." "$(pv skip_$_t)"
done
# THE CONTROL. `' is not on the list and its content IS converted -- without
# this case, all eight above would pass on an implementation that skipped every
# element it met.
check_eq $P/skip-control "“a” — b…" \
  "$(pv skip_control)"
# NESTING, which is why the module keeps a STACK rather than a boolean: a
# `' closing inside a `
' must NOT re-enable conversion for the rest
# of the outer element. `"c" -- d' is still inside the pre and still untouched;
# `"e" -- f' is outside it and converted.
check_eq $P/skip-nested \
  "
\"a\" \"b\" \"c\" -- d
“e” — f" \ "$(pv skip_nested)" # THE LIST IS REPLACEABLE, which is what the shipped script's `-s' does: with # the list set to `q' alone, `' is skipped and `
' -- no longer on it --
# is converted. The reversal in both directions is the assertion.
check_eq $P/skip-custom \
  "\"a\" -- b 
“c” — d
" "$(pv skip_custom)" # ...and the probe put it back, so every case above and below is about the # module as shipped. check_eq $P/skip-restored "pre,samp,code,tt,kbd,script,style,math" \ "$(pv skip_restored)" # ------------------------------------------------------------- the corners --- # A function that returned its argument untouched would fail every behavioural # case above; one that raised on an empty string would fail none of them. The # corners are asserted rather than assumed. check_eq $P/empty "<>" "$(pv empty)" check_eq $P/plain "nothing to do here" "$(pv plain)" # A LONE APOSTROPHE TOKEN, which the function special-cases: whether it curls # open or closed depends on the LAST CHARACTER OF THE PREVIOUS TEXT TOKEN, # carried ACROSS a tag boundary. That one variable is the only state # `smartypants()' keeps between tokens and nothing else in this suite reaches # it; the pair differs by a single space in the input. check_eq $P/lone-quote-after-word "x’y" "$(pv lone_quote_after_word)" check_eq $P/lone-quote-after-space "x ‘y" "$(pv lone_quote_after_space)" # THE DECADE ABBREVIATION, upstream's own named special case: the apostrophe in # `'80s' opens by the general rule and must be forced closed by a rule of its # own. MEASURED: it comes out as ‘ -- the OPENING quote -- so the # special case does not fire here. That is upstream 2.0.0's behaviour on this # input and it is recorded as what it does, not as what its comment intends. check_eq $P/decade "back in the ‘80s" "$(pv decade)" # AN HTML COMMENT is a tag token and is left alone -- unless it CONTAINS `--', # which is malformed HTML, and which the tokenizer therefore re-types as TEXT # and converts. Both branches, because the second mangles the comment delimiters # themselves and a reader meeting it in the wild needs it written down. check_eq $P/comment-plain " “b”" \ "$(pv comment_plain)" check_eq $P/comment-dashes " “c”" \ "$(pv comment_dashes)" # ------------------------------------------------------------ the CLI ------ # THE SHIPPED SCRIPT, RUN. Everything above is about the module; these run # /usr/bin/smartypants as an executable, which is a separate deliverable # reached through its shebang. `script-shebang' and `env-present' above are what # a red here starts from. # # The fixture is written to a file and PIPED in. Both halves matter: the script # reads stdin when given no FILE argument, and a pipe forks, so this never # performs a top-level stdin redirect on the virtiofs root (brintos/linux#73). FIX='"He said, '\''don'\''t'\'' -- really..." and more' printf '%s' "$FIX" > "$W/fix" check_eq $P/cli-fixture "$(pv fixture)" "$(cat "$W/fix")" # The version, from the script's own `-v'. It reads `smartypants.__version__', # so this is also the case that says the script imported the module this image # installed rather than carrying a copy. check_eq $P/cli-version "2.0.0" "$(smartypants --version 2>&1)" # THE SCRIPT AGREES WITH THE API, byte for byte, over the same fixture. That is # the whole claim: the CLI is a front end, not a second implementation. check_eq $P/cli-default "$(pv fixture_default)" "$(cat "$W/fix" | smartypants 2>/dev/null)" # `-a' takes flag NAMES, and a digit is expanded to `set' -- which is # where the attribute numbers above become observable from a shell. `-a 2' # must reproduce the set2 dash mapping exactly. printf '%s' 'a -- b --- c' > "$W/dash" check_eq $P/cli-attr-set2 "$(pv dashes_set2)" \ "$(cat "$W/dash" | smartypants -a 2 2>/dev/null)" # ...and a single named flag, which is the `-a qw' path from the header. check_eq $P/cli-attr-e "$(pv only_e)" \ "$(printf '%s' "\"q\" 'r' -- ... \`\`b'' \`s'" | smartypants -a e 2>/dev/null)" # `-s' replaces the skip list, which is the CLI half of `skip-custom'. check_eq $P/cli-skip "$(pv skip_custom)" \ "$(printf '%s' '"a" -- b
"c" -- d
' | smartypants -s q 2>/dev/null)" # AN UNKNOWN FLAG IS AN ERROR, not a silent no-op: the script warns and exits 1. # A caller who typoed `-a' would otherwise get unconverted text and no signal. check_rc $P/cli-bad-attr 1 sh -c 'printf x | smartypants -a z' # The MESSAGE, separately from the status. `check_out' requires its command to # exit 0 and this one deliberately exits 1, so the `exit 0' is what keeps this # case about the text; `cli-bad-attr' directly above is the one asserting the # status, and neither would be enough alone. check_out $P/cli-bad-attr-msg "Unknown attribute: z" \ sh -c 'printf x | smartypants -a z 2>&1; exit 0' rm -rf "$W"