. "$BST_LIB" # libspiro 20240902 -- Raph Levien's clothoid ("spiro") splines, as meta-oe # ships them from recipes-graphics/fontforge. A font editor hands it a sequence # of spiro control points and it solves for the curve of least curvature # variation through them, emitting the result as bezier segments through a # caller-supplied bezctx callback table. # # --------------------------------------------------------------------------- # WHY THIS SUITE HAS A TOLERANCE, AND WHY IT IS 1e-6 # # Unlike a table-driven library, spiro ITERATES. run_spiro0 solves a non-linear # system by Newton's method and stops when the step norm falls below 1e-12 # (spiro.c, solve_spiro). The converged answer is pinned to about that in the # parameter space, and the bezier coordinates derived from it inherit it -- # PROVIDED libm agrees. sin, cos, atan2 and hypot on the wasm32 target need not # be bit-identical to glibc's on x86_64, and the iteration amplifies any # disagreement a little. So an exact comparison of the kind 78-liblinebreak can # make would be wrong here: it would go red on a correct port. # # 1e-6 absolute, over coordinates of order 1..1000, sits six orders above the # solver's own convergence floor and far below anything a real defect produces. # A wasm32 double path that is actually broken does not miss by 1e-5 -- it # returns NaN, fails to converge (which prints to stderr, and this suite checks # stderr), or misses by whole units. The tolerance is wide enough to absorb # libm, and nowhere near wide enough to absorb a bug. # # WHAT IS ACTUALLY BEING MEASURED. libspiro installs no executable, so without # a cross-linked consumer this suite could only assert that an archive and three # headers exist -- which would pass just as happily if every function in the # archive trapped on first call. data/spiro-probe is that consumer: src/ # spiro_probe.c cross-linked against the very archive this port installs and # reached through the way a font editor reaches it. # # THE ANCHORED CASES EXIST BECAUSE AN ORACLE CAN BE WRONG. The bulk of this # file compares target output against data/spiro-native.txt, which catches any # disagreement between the two machines but would NOT catch a control captured # from an already-broken build. Three kinds of case therefore do not consult the # control at all: # # * the X residuals. Spiro is INTERPOLATING -- the curve passes through every # control point -- and its formulation is defined on chord lengths and # turning angles, so it commutes exactly with translation, uniform scaling # and reflection. The probe re-solves the same geometry under each of those # transforms and prints how far the answer is from the transform of the # original answer. Every one of those residuals is 0 in exact arithmetic, as # a property of the mathematics rather than of this build. # * the degenerate contours. A two-point contour is a straight line and a # four-corner closed contour is a square: both are emitted without solving # anything, so their coordinates are the INPUT, exactly, and are anchored # here as literals. # * the constants and replies -- point-type codes and ncq flags read off # spiroentrypoints.h, and the documented 0 reply for invalid input. # --------------------------------------------------------------------------- # A root prefix, empty on the target and set only when this suite is validated # NATIVELY under dash before it ships. Writing /usr/lib and /usr/include needs # real root, so a native run stages them under a temporary tree and points this # at it. The runner never sets it, so on the image these are exactly the # absolute paths. R="${BST_SPIRO_ROOT:-}" ARCHIVE=$R/usr/lib/libspiro.a INCDIR=$R/usr/include PCFILE=$R/usr/lib/pkgconfig/libspiro.pc PROBE=$BST_DATA/spiro-probe NATIVE=$BST_DATA/spiro-native.txt # The comparison tolerance, in the units the probe prints (curve coordinates). TOL=0.000001 if [ ! -f "$ARCHIVE" ]; then bst_skip spiro/present "libspiro not installed on this image" exit 0 fi bst_ok spiro/present # --- the package is whole ---------------------------------------------------- check spiro/hdr-spiro test -f "$INCDIR/spiro.h" check spiro/hdr-bezctx test -f "$INCDIR/bezctx.h" check spiro/hdr-entrypoints test -f "$INCDIR/spiroentrypoints.h" check spiro/staticlib test -f "$ARCHIVE" check spiro/pkgconfig test -f "$PCFILE" # Shape, not absence: this arm converts to shared libraries per recipe # (BRINTOS-1740), so a .so here is a conversion, not a policy breach. bst_lib_shape spiro/no-shared "$R/usr/lib/libspiro.so*" # The .pc must describe the paths a cross consumer will actually be handed, so # libdir is asserted as the LITERAL the cross build ships. Upstream's template # carries @libdir@, and what that becomes depends on who ran configure: a bare # native ./configure leaves the reference "${exec_prefix}/lib", but Yocto's # autotools class passes --libdir explicitly, so the substitution is already # resolved by the time the file is packaged. Asserting the reference form is # what goes red on a correct image -- measured on the staged rootfs, not on a # native configure, which is the only oracle that can answer this. check spiro/pc-libdir grep -q '^libdir=/usr/lib$' "$PCFILE" check spiro/pc-lspiro grep -q '^Libs:.*-lspiro' "$PCFILE" # --- the probe and its control ship together --------------------------------- if [ ! -f "$PROBE" ]; then bst_notok spiro/probe-present "$PROBE missing -- the port cannot be functionally verified" exit 0 fi bst_ok spiro/probe-present if [ ! -f "$NATIVE" ]; then bst_notok spiro/native-present "$NATIVE missing -- no oracle to compare against" exit 0 fi bst_ok spiro/native-present # --- run the consumer once --------------------------------------------------- # brintos-selftests' do_install stages data/* at 0644, and the deployed rootfs is # read-only at browser runtime, so the probe cannot simply be executed where it # lies -- stage an executable copy under TMPDIR, as every probe-driven suite in # this package does. tmp=${TMPDIR:-/tmp}/bst-spiro.$$ _p=$tmp.bin cp "$PROBE" "$_p" 2>/dev/null && chmod 0755 "$_p" 2>/dev/null if [ ! -x "$_p" ]; then bst_notok spiro/probe-exec "could not stage an executable copy of the probe" rm -f "$_p" exit 0 fi bst_ok spiro/probe-exec "$_p" > "$tmp.out" 2> "$tmp.err"; rc=$? rm -f "$_p" check_eq spiro/probe-rc 0 "$rc" # A truncated run must be distinguishable from a run that legitimately produced # these lines, so the probe prints a terminator and this is checked before any # comparison. Without it a crash half way through would silently reduce the # number of cases rather than fail one. if [ "$(tail -n 1 "$tmp.out" 2>/dev/null)" = "PROBE-END" ]; then bst_ok spiro/probe-complete else bst_notok spiro/probe-complete "no PROBE-END terminator: $(_bst_1line "$(tail -n 3 "$tmp.out" 2>/dev/null)")" fi want_n=$(wc -l < "$NATIVE" | tr -d ' ') got_n=$(wc -l < "$tmp.out" | tr -d ' ') check_eq spiro/line-count "$want_n" "$got_n" # --- the anchored cases: the header and the mathematics, not the control ----- sp_get() { awk -v k="$1" '$1==k { $1=""; sub(/^ /,""); print; exit }' "$tmp.out"; } # A residual that is zero in exact arithmetic. Asserted as |v| <= TOL rather # than as a string so the case states what it means. affine_residual() reports # -1 when the two runs emitted a different NUMBER of callbacks and -2 when they # emitted different callback TYPES; both have modulus far above the tolerance, # so they land here as the failures they are rather than as near-zero values. sp_small() { _v=$(sp_get "$1") if [ -z "$_v" ]; then bst_notok "spiro/$2" "case $1 absent from probe output" elif awk -v v="$_v" -v t="$TOL" 'BEGIN { if (v<0) v=-v; exit !(v<=t) }'; then bst_ok "spiro/$2" else bst_notok "spiro/$2" "residual $_v exceeds tolerance $TOL" fi } # The library's own version string, compiled into the archive. This is the one # case that would catch the image having been built against a DIFFERENT # libspiro than the control was captured from -- every coordinate below would # then differ for a reason that is not a defect. check_eq spiro/anchor-version "1.4" "$(sp_get I01-version)" # The public struct a caller declares its own point arrays with. Two doubles # and a char, 8-byte aligned: 24 on x86_64 and on wasm32 alike. A width that # changed under a new arm would corrupt every caller silently. check_eq spiro/anchor-sizeof-cp 24 "$(sp_get I02-sizeof-spiro-cp)" check_eq spiro/anchor-double-width 8 "$(sp_get I18-double-width)" # The point-type codes, read off spiroentrypoints.h as characters. These are # the API: a consumer writes 'v' or 'o' into .ty and the library switches on it. check_eq spiro/anchor-ty-corner 118 "$(sp_get I03-ty-corner)" # 'v' check_eq spiro/anchor-ty-g4 111 "$(sp_get I04-ty-g4)" # 'o' check_eq spiro/anchor-ty-g2 99 "$(sp_get I05-ty-g2)" # 'c' check_eq spiro/anchor-ty-left 91 "$(sp_get I06-ty-left)" # '[' check_eq spiro/anchor-ty-right 93 "$(sp_get I07-ty-right)" # ']' check_eq spiro/anchor-ty-end 122 "$(sp_get I08-ty-end)" # 'z' check_eq spiro/anchor-ty-open 123 "$(sp_get I09-ty-open)" # '{' check_eq spiro/anchor-ty-end-open 125 "$(sp_get I10-ty-end-open)" # '}' check_eq spiro/anchor-ty-anchor 97 "$(sp_get I11-ty-anchor)" # 'a' check_eq spiro/anchor-ty-handle 104 "$(sp_get I12-ty-handle)" # 'h' # The ncq flag values, likewise from the header. check_eq spiro/anchor-ncq-last-knot 256 "$(sp_get I13-ncq-last-knot)" check_eq spiro/anchor-ncq-retro 1024 "$(sp_get I14-ncq-retro)" check_eq spiro/anchor-ncq-arc-maybe 8192 "$(sp_get I15-ncq-arc-maybe)" check_eq spiro/anchor-ncq-quad0 16384 "$(sp_get I16-ncq-quad0)" check_eq spiro/anchor-ncq-internal 512 "$(sp_get I17-ncq-internal)" # The double path itself, before any spline is solved. If these are wrong then # every coordinate in this file is meaningless, and the failure is legible here # rather than as a thousand red comparisons. atan2(1,2) is a mathematical # constant; hypot(3,4) and sin^2+cos^2 are exact. check_eq spiro/anchor-libm-atan2 0.463647609 "$(sp_get I19-libm-atan2)" check_eq spiro/anchor-libm-hypot 5.000000000 "$(sp_get I20-libm-hypot)" check_eq spiro/anchor-libm-sincos 1.000000000 "$(sp_get I21-libm-sincos)" # The degenerate contours, whose output is the input and involves no solving. # A two-point open contour is one moveto and one lineto at exactly the two # points given; anything else means the trivial path is being routed through # the solver. check_eq spiro/anchor-line-count 3 "$(sp_get F01-count)" check_eq spiro/anchor-line-move "m 10.000000000 20.000000000 open=1" "$(sp_get F01-op000)" check_eq spiro/anchor-line-to "l 310.000000000 220.000000000" "$(sp_get F01-op002)" # A closed contour of four SPIRO_CORNER points is a square: four straight # edges, each landing exactly on the next corner, and open=0 because it closes. check_eq spiro/anchor-square-count 9 "$(sp_get B01-count)" check_eq spiro/anchor-square-move "m 0.000000000 0.000000000 open=0" "$(sp_get B01-op000)" check_eq spiro/anchor-square-e1 "l 100.000000000 0.000000000" "$(sp_get B01-op002)" check_eq spiro/anchor-square-e2 "l 100.000000000 100.000000000" "$(sp_get B01-op004)" check_eq spiro/anchor-square-e3 "l 0.000000000 100.000000000" "$(sp_get B01-op006)" check_eq spiro/anchor-square-e4 "l 0.000000000 0.000000000" "$(sp_get B01-op008)" # The documented replies. 1 on success from every entry point that has one, and # 0 -- not a crash, not a partial path -- for each rejected input. The last is # a '[' whose ']' does not immediately follow it, which is a malformed contour # rather than a hard case: it must be refused, and refused without a callback. check_eq spiro/anchor-rc-open 1 "$(sp_get A01-rc)" check_eq spiro/anchor-rc-closed 1 "$(sp_get B01-rc)" check_eq spiro/anchor-rc-bracket 1 "$(sp_get E01-rc)" check_eq spiro/anchor-rc-anchor 1 "$(sp_get G01-rc)" check_eq spiro/anchor-solved 1 "$(sp_get L01-solved)" check_eq spiro/anchor-lsbez-alloc 1 "$(sp_get M01-alloc)" check_eq spiro/anchor-fail-n-zero 0 "$(sp_get K01-n-zero-rc)" check_eq spiro/anchor-fail-n-neg 0 "$(sp_get K02-n-neg-rc)" check_eq spiro/anchor-fail-ncq-neg 0 "$(sp_get K03-ncq-neg-rc)" check_eq spiro/anchor-fail-tagged 0 "$(sp_get K04-tagged-ncq-neg-rc)" check_eq spiro/anchor-fail-bracket "0 count 0" "$(sp_get K05-unpaired-bracket-rc)" # The equivariance and interpolation residuals -- the part of this suite that an # incorrect control cannot make vacuously green. sp_small X01-translate-residual anchor-equivar-translate sp_small X02-scale-residual anchor-equivar-scale sp_small X03-reflect-residual anchor-equivar-reflect sp_small X04-interp-open anchor-interp-open sp_small X05-interp-closed anchor-interp-closed sp_small X06-interp-g2 anchor-interp-g2 sp_small X07-interp-mixed anchor-interp-mixed sp_small X08-interp-line anchor-interp-line sp_small X09-determinism anchor-determinism check_eq spiro/anchor-input-unmodified 1 "$(sp_get X10-input-unmodified)" # --- the control comparison: one case per probe line ------------------------- # # Both files carry the same ids in the same order by construction (same probe # source, same sequence of printf calls), so this is a single awk pass rather # than one subprocess per line -- 294 awk invocations would be slow inside the # guest. Each field is compared numerically when BOTH sides parse as a number # and textually otherwise, which is what lets "nan" or "inf" against a number # fail as the mismatch it is instead of being coerced to one. # # A target run that ended early leaves the remaining control lines unmatched, # and each is reported against its own id rather than silently shortening the # suite. awk -v tol="$TOL" -v ctl="$NATIVE" ' function isnum(s) { return s ~ /^-?([0-9]+(\.[0-9]*)?|\.[0-9]+)([eE][-+]?[0-9]+)?$/ } function abs(x) { return x < 0 ? -x : x } { ng = split($0, g, " ") if ((getline w < ctl) <= 0) { printf "notok %s no-control-line-for-this-case\n", g[1]; next } nw = split(w, a, " ") if (a[1] != g[1]) { printf "notok %s out-of-step want-id=%s got-id=%s\n", a[1], a[1], g[1]; next } if (nw != ng) { printf "notok %s field-count want=%d got=%d\n", a[1], nw-1, ng-1; next } bad = ""; worst = 0 for (i = 2; i <= nw; i++) { if (isnum(a[i]) && isnum(g[i])) { d = abs(a[i] - g[i]) if (d > tol && d > worst) { worst = d bad = sprintf("field%d want=%s got=%s delta=%.3g", i-1, a[i], g[i], d) } } else if (a[i] != g[i] && bad == "") { bad = sprintf("field%d want=%s got=%s", i-1, a[i], g[i]) } } if (bad == "") printf "ok %s -\n", a[1] else printf "notok %s %s\n", a[1], bad } END { while ((getline w < ctl) > 0) { split(w, a, " "); printf "notok %s target-ended-before-this-case\n", a[1] } } ' "$tmp.out" > "$tmp.cmp" while IFS= read -r line; do _v=${line%% *}; _r=${line#* } _id=${_r%% *}; _d=${_r#* } if [ "$_v" = ok ]; then bst_ok "spiro/$_id"; else bst_notok "spiro/$_id" "$_d"; fi done < "$tmp.cmp" # Nothing should reach stderr: the probe writes only to stdout, so anything here # came either from the runtime (a trap message, a loader complaint) or from # spiro's own non-convergence report, which it prints to stderr and which no # contour driven above is supposed to trigger. if [ -s "$tmp.err" ]; then bst_notok spiro/clean-stderr "$(_bst_1line "$(cat "$tmp.err")")" else bst_ok spiro/clean-stderr fi rm -f "$tmp.out" "$tmp.err" "$tmp.cmp"