. "$BST_LIB" # BST-TIMEOUT: 300 # the probe trains 1000 epochs; ~20s on target, 300 is headroom # libfann 2.2.0+git -- the Fast Artificial Neural Network library (meta-oe # recipes-support, a git recipe pinned at SRCREV 7ec1fc7e). What ships is four # archives -- libfann.a, libfloatfann.a, libdoublefann.a, libfixedfann.a -- the # fourteen headers, fann.pc and the two cmake config files. There is no # installed executable: fann's INSTALL() names a RUNTIME DESTINATION but never # adds an ADD_EXECUTABLE target to it. # # --------------------------------------------------------------------------- # WHY THIS SUITE RUNS A PROBE # # Because there is nothing else to run. The ticket's requirement -- "train a # fixed network on the bundled XOR data set with a pinned seed, save it, reload # it and re-run: the reloaded network's outputs must match the trained one # exactly" -- is in-process from end to end, and libfann installs no command # line to reach any of it from. So coverage is data/libfann-probe # (src/libfann_probe.c), cross-linked with wasmcc-glibc against THE VERY # ARCHIVE THAT SHIPS, driven with data/libfann-xor.data (examples/xor.data from # the same pinned commit -- the ticket's "the data set ships with the source, so # there is nothing to fetch"). # # fann DOES ship tests/, but they are gtest and this port gates them off: the # vendored googletest needs a C++ standard library that a C library's sysroot # does not have (0002-cmake-gate-the-bundled-googletest-behind-BUILD_TESTIN). # So there is no upstream suite runnable here even in principle. # # --------------------------------------------------------------------------- # THE ORACLE IS AN x86_64 CONTROL, PLUS ONE THAT IS NOT # # data/libfann-native.txt is the same probe's output against a build of the same # commit with the same two patches and the same cmake options # (src/libfann_mknative.sh). Every LF- line is an integer, a boolean, or a float # produced by multiply-add on exactly-representable operands, so each must match # byte for byte. # # The LF-linear-* group is the one that does not merely compare. The probe sets # every weight equal and both activations to FANN_LINEAR at steepness 1, which # reduces fann_run to # # hidden = w*(x0 + x1 + 1) output = w*(3*hidden + 1) # # and then checks fann's answer against that closed form computed separately in # the probe. Because every weight is equal it does not depend on fann's # connection ordering, so it is an oracle rather than a restatement of fann's # internals -- the same role RFC 1321's vector plays in the libubox suite. It # calls no libm. # # WHAT IS DELIBERATELY NOT COMPARED. The LFX- lines: the trained output values # and the MSE. fann_init_weights seeds the network through pow() and sqrt(), and # two libms are not obliged to round those identically, so a control comparison # there would go red on a correct build the day either libm changed. They are # not left unasserted -- LF-xor-correct says the training converged and # LFX-bit-fail says by how much, and the two together are a stronger statement # than pinning the floats would be. See the bit-fail case at the end. # # --------------------------------------------------------------------------- # WHAT A RED MEANS, BY GROUP # # archive/header packaging. The port's INSTALL rule is rewritten by # 0001, so this group is what notices if that patch ever # stops matching a newer fann. # cmp-data-* fann's own training-file parser, over a 36-byte file. # cmp-linear-* the forward pass, as pure multiply-add. A red here is # libfann miscompiled on this arm and nothing else -- no # syscall, no libm, no allocation pattern is involved. # cmp-reload-exact THE TICKET'S ASSERTION. Both sides of it were produced by # this run on this arm, so it needs no control at all and a # red is a serialiser defect, full stop. # cmp-save-* the writer, compared as bytes rather than through outputs. # cmp-xor-correct the training converged. # # There is no group here that touches the kernel, so unlike libubox's uloop this # suite has no platform-held cases and no expected skips: on an image that ships # libfann every case above is a real assertion, and all of them are expected # green. libd=/usr/lib incd=/usr/include DATA=$BST_DATA NATIVE=$DATA/libfann-native.txt PROBE=$DATA/libfann-probe XOR=$DATA/libfann-xor.data TMP=${TMPDIR:-/tmp} nl=' ' if [ ! -f "$libd/libfann.a" ]; then bst_skip libfann/present "libfann not installed on this image" exit 0 fi bst_ok libfann/present hasfile() { if [ -s "$2" ]; then bst_ok "$1"; else bst_notok "$1" "absent or empty: $2"; fi; } hasline() { case "$nl$3$nl" in *"$nl$2$nl"*) bst_ok "$1" ;; *) bst_notok "$1" "not in archive: $2" ;; esac; } # --- payload shape ----------------------------------------------------------- # All four archives, because 0001 rewrites the INSTALL(TARGETS ...) rule that # produces them: if that patch stops applying to a newer fann, the shared arm's # rule comes back and -staticdev ships short. The recipe asserts the same four # at do_install; this is the same claim made where a reviewer can see it. hasfile libfann/archive "$libd/libfann.a" hasfile libfann/archive-float "$libd/libfloatfann.a" hasfile libfann/archive-double "$libd/libdoublefann.a" hasfile libfann/archive-fixed "$libd/libfixedfann.a" hasfile libfann/header-fann "$incd/fann.h" hasfile libfann/header-float "$incd/floatfann.h" hasfile libfann/header-double "$incd/doublefann.h" hasfile libfann/header-fixed "$incd/fixedfann.h" hasfile libfann/header-data "$incd/fann_data.h" hasfile libfann/header-train "$incd/fann_train.h" hasfile libfann/header-io "$incd/fann_io.h" hasfile libfann/header-cascade "$incd/fann_cascade.h" hasfile libfann/pkgconfig "$libd/pkgconfig/fann.pc" hasfile libfann/cmake-config "$libd/cmake/fann/fann-config.cmake" # The distro is static-only, and libfann is a reason that needed a patch: # upstream declares all four libraries ADD_LIBRARY( SHARED ...), which # BUILD_SHARED_LIBS cannot override. A shared object here means # 0001-cmake-follow-BUILD_SHARED_LIBS-instead-of-hardcoding-.patch stopped # applying and the image grew objects wasmld has no stable ABI for. if [ -e "$libd/libfann.so" ] || [ -e "$libd/libfann.so.2" ] || \ [ -e "$libd/libfloatfann.so" ] || [ -e "$libd/libdoublefann.so" ] || \ [ -e "$libd/libfixedfann.so" ]; then bst_notok libfann/no-shared-object "a libfann shared object shipped on a static-only distro" else bst_ok libfann/no-shared-object fi # --- symbol census ----------------------------------------------------------- # One name per subsystem, so a partially-linked archive is legible rather than # merely "incomplete". arcsyms=$(grep -aoE 'fann_[a-z0-9_]*' "$libd/libfann.a" 2>/dev/null | sort -u) hasline libfann/sym-create-standard fann_create_standard "$arcsyms" hasline libfann/sym-run fann_run "$arcsyms" hasline libfann/sym-train-on-data fann_train_on_data "$arcsyms" hasline libfann/sym-init-weights fann_init_weights "$arcsyms" hasline libfann/sym-save fann_save "$arcsyms" hasline libfann/sym-create-from-file fann_create_from_file "$arcsyms" hasline libfann/sym-read-train fann_read_train_from_file "$arcsyms" hasline libfann/sym-disable-seed fann_disable_seed_rand "$arcsyms" hasline libfann/sym-test-data fann_test_data "$arcsyms" hasline libfann/sym-set-weights fann_set_weights "$arcsyms" # The serial training path, and its absence as a case of its own. The recipe # passes -DDISABLE_PARALLEL_FANN=ON, so fann_train_epoch_irpropm is built and # fann_train_epoch_irpropm_parallel is not. If the parallel twin ever appears # here, the archive carries GOMP calls that nothing on this image resolves, and # the failure would otherwise land in whatever next links libfann.a. hasline libfann/sym-train-epoch fann_train_epoch_irpropm "$arcsyms" case "$nl$arcsyms$nl" in *"${nl}fann_train_epoch_irpropm_parallel$nl"*) bst_notok libfann/no-openmp "fann_train_epoch_irpropm_parallel is in the archive -- OpenMP was detected for the target, so libfann.a carries unresolvable GOMP calls" ;; *) bst_ok libfann/no-openmp ;; esac # --- the library RUNNING, via the cross-linked probe ------------------------- if [ ! -s "$NATIVE" ]; then bst_notok libfann/native-present "$NATIVE missing -- no oracle to compare against" exit 0 fi bst_ok libfann/native-present if [ ! -s "$XOR" ]; then bst_notok libfann/xor-data-present "$XOR missing -- the probe has no training set" exit 0 fi bst_ok libfann/xor-data-present # do_install stages data/* at 0644 and the deployed rootfs is read-only at # browser runtime, so the probe cannot be executed where it lies. if [ ! -f "$PROBE" ]; then bst_notok libfann/probe-present "$PROBE absent from the image" exit 0 fi _p=$TMP/bst-libfann-probe.$$ cp "$PROBE" "$_p" 2>/dev/null && chmod 0755 "$_p" 2>/dev/null if [ ! -x "$_p" ]; then bst_notok libfann/probe-exec "could not stage an executable copy of $PROBE" rm -f "$_p" exit 0 fi bst_ok libfann/probe-exec out=$TMP/bst-libfann.$$ timeout 280 "$_p" "$XOR" "$TMP" > "$out.out" 2>"$out.err"; prc=$? rm -f "$_p" check_eq libfann/probe-rc 0 "$prc" # The probe prints LF-done last and unconditionally, so a run missing it stopped # early -- and every early exit in the probe is itself a reported LF- line, so # the pair localises the stop rather than merely detecting it. if [ "$(grep -c '^LF-done 1' "$out.out" 2>/dev/null)" = "1" ]; then bst_ok libfann/probe-complete else bst_notok libfann/probe-complete "no terminator; last saw: $(_bst_1line "$(tail -n 2 "$out.out" 2>/dev/null)")" fi lf_get() { awk -v k="$1" '$1==k { $1=""; sub(/^ /,""); print; exit }' "$out.out"; } # --- tag-by-tag against the native control ----------------------------------- # One awk over both files rather than a shell loop of ~30 awk calls: process # spawn is expensive on this target and the whole comparison is in-memory. awk ' NR == FNR { if ($0 !~ /^LF-/) next k = $1; $1 = ""; sub(/^ /, ""); W[k] = $0; order[++n] = k; next } /^LF-/ { k = $1; $1 = ""; sub(/^ /, ""); G[k] = $0; seen[k] = 1 } END { for (i = 1; i <= n; i++) { k = order[i]; t = substr(k, 4) if (!(k in seen)) { printf "BST not ok libfann/cmp-%s : absent from target output\n", t; continue } if (G[k] == W[k]) printf "BST ok libfann/cmp-%s\n", t else printf "BST not ok libfann/cmp-%s : want:%s got:%s\n", t, W[k], G[k] } } ' "$NATIVE" "$out.out" # --- anchored: values libfann's own contract fixes, not the control ---------- # These duplicate comparisons above ON PURPOSE. They are the invariants a # consumer actually depends on, written as literals, so a control regenerated # against a broken build could not quietly move them. # # The ticket's requirement, stated as itself. check_eq libfann/anchor-reload-exact 1 "$(lf_get LF-reload-exact)" # Re-serialising the reloaded network reproduces the file it came from. Distinct # from reload-exact: that compares outputs, so a field the writer drops but # fann_run never reads would survive it and this would not. check_eq libfann/anchor-save-idempotent 1 "$(lf_get LF-save-idempotent)" # The network actually learned XOR -- all four patterns on the right side of 0. check_eq libfann/anchor-xor-correct 4 "$(lf_get LF-xor-correct)" # The forward pass against the closed form, per pattern. These are the four # strongest lines in the suite: exact float bit patterns with an oracle that is # not fann's own arithmetic. -0.25, 1.25, 1.25, 2.75. check_eq libfann/anchor-linear-oracle-0 1 "$(lf_get LF-linear-oracle-0)" check_eq libfann/anchor-linear-oracle-1 1 "$(lf_get LF-linear-oracle-1)" check_eq libfann/anchor-linear-oracle-2 1 "$(lf_get LF-linear-oracle-2)" check_eq libfann/anchor-linear-oracle-3 1 "$(lf_get LF-linear-oracle-3)" check_eq libfann/anchor-linear-out-0 be800000 "$(lf_get LF-linear-out-0)" check_eq libfann/anchor-linear-out-3 40300000 "$(lf_get LF-linear-out-3)" # The topology fann builds for a 2-3-1 request, including its bias neurons: # 3 hidden x 3 inputs+bias = 9, plus 1 output x 3 hidden+bias = 4. check_eq libfann/anchor-num-input 2 "$(lf_get LF-num-input)" check_eq libfann/anchor-num-output 1 "$(lf_get LF-num-output)" check_eq libfann/anchor-num-layers 3 "$(lf_get LF-num-layers)" check_eq libfann/anchor-neurons 8 "$(lf_get LF-total-neurons)" check_eq libfann/anchor-connections 13 "$(lf_get LF-total-connections)" # fann_type is float on this variant; a 4 here is also what makes the LF- float # comparisons meaningful as 32-bit patterns. check_eq libfann/anchor-fann-type 4 "$(lf_get LF-fann-type-size)" # The training set as fann's parser sees it: 4 rows, 2 in, 1 out, and the XOR # truth table itself in IEEE-754 (-1 = bf800000, +1 = 3f800000). check_eq libfann/anchor-data-len 4 "$(lf_get LF-data-len)" check_eq libfann/anchor-data-row-0 "bf800000 bf800000 bf800000" "$(lf_get LF-data-row-0)" check_eq libfann/anchor-data-row-3 "3f800000 3f800000 bf800000" "$(lf_get LF-data-row-3)" # --- the values that legitimately differ, asserted rather than ignored ------- # The trained floats are not pinned (see the header), but their QUALITY is. # fann_get_bit_fail counts the outputs further than bit_fail_limit (0.01) from # their target, so 0 says every one of the four is within 0.01 -- which is a # tighter statement than any sign check, and it is an integer, so no libm # difference can move it. A non-zero here on a green everything-else means # training ran but did not converge in 1000 epochs, which is a real finding # about this arm's float behaviour rather than a packaging problem. _bf=$(lf_get LFX-bit-fail) case "$_bf" in ''|*[!0-9]*) bst_notok libfann/train-converged "unreadable bit-fail: '${_bf:-}'" ;; 0) bst_ok libfann/train-converged ;; *) bst_notok libfann/train-converged "$_bf of 4 outputs outside the 0.01 bit-fail limit after 1000 epochs" ;; esac rm -f "$out.out" "$out.err"