#!/bin/bash # # Copyright (C) 2026 Danilo M. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # # Logic self-check for test-build. Covers the pure, host-side parts: # dependency resolution, override application, unknown-dep -> UNMET, cache # decisions, and BLOCKED-BY-DEP propagation. No docker. # # Run: bash test-logic.sh # set -uo pipefail SCRIPT="$(dirname "$0")/test-build" T=$(mktemp -d) BASE_DB=$(mktemp -d); mkdir -p "$BASE_DB" cleanup() { rm -rf "$T" "$BASE_DB"; } trap cleanup EXIT # Source the script without running main(). Sourcing re-runs the CONFIG block, # so set test vars AFTER the source. LIB=$(mktemp) sed '/^main "\$@"$/d' "$SCRIPT" > "$LIB" # shellcheck disable=SC1090 source "$LIB" 2>/dev/null rm -f "$LIB" pass=0; fail=0 ok() { echo " ok: $1"; ((pass++)); return 0; } bad() { echo " FAIL: $1"; ((fail++)); return 0; } # Placeholder assertion so this file runs before any logic exists. ok "script sources without executing main" # --- version selection ------------------------------------------------------ SBO_TREE_CURRENT="/trees/current" SBO_TREE_STABLE="/trees/stable" IMAGE_CURRENT="sbo-testbuild:current" IMAGE_STABLE="sbo-testbuild:15.0" VERSION_ID="current"; select_version_paths [[ "$ACTIVE_TREE" == "/trees/current" ]] && ok "current -> current tree" || bad "current tree wrong: [$ACTIVE_TREE]" [[ "$ACTIVE_IMAGE" == "sbo-testbuild:current" ]] && ok "current -> current image" || bad "current image wrong: [$ACTIVE_IMAGE]" VERSION_ID="15.0"; select_version_paths [[ "$ACTIVE_TREE" == "/trees/stable" ]] && ok "15.0 -> stable tree" || bad "stable tree wrong: [$ACTIVE_TREE]" [[ "$ACTIVE_IMAGE" == "sbo-testbuild:15.0" ]] && ok "15.0 -> stable image" || bad "stable image wrong: [$ACTIVE_IMAGE]" # --- resolution ------------------------------------------------------------- # Fake SBo tree under one category. mk "". mk() { mkdir -p "$T/cat/$1"; echo "REQUIRES=\"$2\"" > "$T/cat/$1/$1.info"; } mk c "" mk b "c" mk a "b %README%" mk d "nonexistentpkg" mk e "f" mk f "e" mk g "b" SBO_TREE_ROOTS=("$T") # resolver reads this global (set by select in real runs) resolve_target "$T/cat/a" order=""; for x in "${RESOLVED_ORDER[@]}"; do order+="$(basename "$x") "; done order="${order% }" [[ "$order" == "c b a" ]] && ok "topo order c b a" || bad "topo order, got: [$order]" [[ "${HAS_README[$T/cat/a]:-}" == "1" ]] && ok "%README% recorded" || bad "%README% not recorded" [[ ${#UNMET[@]} -eq 0 ]] && ok "no false unmet" || bad "unexpected unmet" resolve_target "$T/cat/d" [[ ${#UNMET[@]} -eq 1 ]] && ok "unmet-dep caught" || bad "unmet-dep missed" # --- resolve_target_dir (target from CWD/path, NOT the SBo tree) ------------- # Local package dir, separate from the fake SBo tree above. LOCALREPO=$(mktemp -d); mkdir -p "$LOCALREPO/mypkg" echo 'REQUIRES=""' > "$LOCALREPO/mypkg/mypkg.info" # 1. absolute path got="$(resolve_target_dir "$LOCALREPO/mypkg" 2>/dev/null)" [[ "$got" == "$LOCALREPO/mypkg" ]] && ok "target: absolute path" || bad "abs path got [$got]" # 2. bare name resolved under CWD (./mypkg/) got="$(cd "$LOCALREPO" && resolve_target_dir "mypkg" 2>/dev/null)" [[ "$got" == "$LOCALREPO/mypkg" ]] && ok "target: bare name in CWD" || bad "bare-in-cwd got [$got]" # 3. CWD itself is the package got="$(cd "$LOCALREPO/mypkg" && resolve_target_dir "mypkg" 2>/dev/null)" [[ "$got" == "$LOCALREPO/mypkg" ]] && ok "target: CWD is the package" || bad "cwd-is-pkg got [$got]" # 4. missing -> failure resolve_target_dir "nosuchpkg" >/dev/null 2>&1 && bad "missing target should fail" || ok "target: missing fails" rm -rf "$LOCALREPO" resolve_target "$T/cat/e" [[ ${#CYCLES[@]} -ge 1 ]] && ok "cycle caught" || bad "cycle missed" # --- installed_in_base ------------------------------------------------------ # Pre-seed the base pkg set (TB_BASE_LOADED=1 bypasses docker). Names are the # bare package name; installed_in_base must match a REQUIRES token against it. TB_BASE_LOADED=1 TB_BASE_PKGS=([python3]=1 [libfoo]=1) installed_in_base python3 && ok "base pkg matched" || bad "base pkg not matched" installed_in_base notthere && bad "absent pkg wrongly matched" || ok "absent pkg not matched" # a dep present in the base resolves as met, not UNMET mk h "python3" resolve_target "$T/cat/h" [[ ${#UNMET[@]} -eq 0 ]] && ok "base-provided dep not unmet" || bad "base dep flagged unmet" TB_BASE_LOADED=0; TB_BASE_PKGS=() # --- overrides -------------------------------------------------------------- VERSION_ID="current" # reset: left at 15.0 by the version-selection checks above OV=$(mktemp) cat > "$OV" <<'EOF' # comment ignored drop: dropme rename: oldname -> newname fetch: fetchme EOF TB_OVERRIDES="$OV" load_overrides [[ "${OV_DROP[dropme]:-}" == "1" ]] && ok "override drop parsed" || bad "drop not parsed" [[ "${OV_RENAME[oldname]:-}" == "newname" ]] && ok "override rename parsed" || bad "rename not parsed" [[ "${OV_FETCH[fetchme]:-}" == "1" ]] && ok "override fetch parsed" || bad "fetch not parsed" # rename maps a token [[ "$(apply_rename oldname)" == "newname" ]] && ok "apply_rename maps" || bad "apply_rename wrong: [$(apply_rename oldname)]" [[ "$(apply_rename untouched)" == "untouched" ]] && ok "apply_rename passthrough" || bad "apply_rename mangled untouched" # drop satisfies an external (no-dir) dep during resolution: not UNMET, absent # from the order. mk k requires a dropme that has no dir in the tree. OV_DROP=([extdrop]=1) mk k "extdrop" resolve_target "$T/cat/k" [[ ${#UNMET[@]} -eq 0 ]] && ok "drop satisfies external dep (not unmet)" || bad "dropped external dep still unmet" seen=""; for x in "${RESOLVED_ORDER[@]}"; do seen+="$(basename "$x") "; done [[ "$seen" != *extdrop* ]] && ok "dropped dep absent from order" || bad "dropped dep in order: [$seen]" load_overrides # restore OV_DROP[dropme] for the order-filter test below # drop filters the order (build a fake order of dirs; dropme is removed) mk dropme "" mk keepme "" RESOLVED_ORDER=("$T/cat/dropme" "$T/cat/keepme") apply_overrides_to_order kept=""; for x in "${RESOLVED_ORDER[@]}"; do kept+="$(basename "$x") "; done kept="${kept% }" [[ "$kept" == "keepme" ]] && ok "drop removes from order" || bad "drop failed, order=[$kept]" # all entries dropped -> RESOLVED_ORDER must be truly empty (not one "" element) OV_DROP=([dropme]=1 [keepme]=1) RESOLVED_ORDER=("$T/cat/dropme" "$T/cat/keepme") apply_overrides_to_order [[ ${#RESOLVED_ORDER[@]} -eq 0 ]] && ok "all-dropped -> empty order" || bad "all-dropped left ${#RESOLVED_ORDER[@]} elem(s): [${RESOLVED_ORDER[*]}]" OV_DROP=() rm -f "$OV" # overrides only apply on current; on 15.0 they are a no-op VERSION_ID="15.0" TB_OVERRIDES="$OV" # file gone; must not error, must clear maps load_overrides [[ ${#OV_DROP[@]} -eq 0 ]] && ok "overrides inert on 15.0" || bad "overrides applied on 15.0" VERSION_ID="current" # --- cache ------------------------------------------------------------------ PKG_CACHE=$(mktemp -d) CACHE_ROOT="$PKG_CACHE/sha256-deadbeef" # simulate a resolved digest namespace mkc() { mkdir -p "$CACHE_ROOT/$1/$2"; : > "$CACHE_ROOT/$1/$2/$3"; } mkc net libfoo "libfoo-1.1-x86_64-1_danix.txz" [[ "$(cache_decision net libfoo 1.1)" == "cached" ]] && ok "cache hit on version match" || bad "cache_decision got [$(cache_decision net libfoo 1.1)]" [[ "$(cache_decision net libfoo 1.2)" == "bump:1.1:1.2" ]] && ok "cache bump reported" || bad "cache bump got [$(cache_decision net libfoo 1.2)]" [[ "$(cache_decision net libbar 1.0)" == "new" ]] && ok "cache new for absent" || bad "cache new got [$(cache_decision net libbar 1.0)]" hit="$(cache_path net libfoo 1.1)" [[ "$hit" == "$CACHE_ROOT/net/libfoo/libfoo-1.1-x86_64-1_danix.txz" ]] && ok "cache_path returns hit" || bad "cache_path got [$hit]" [[ -z "$(cache_path net libfoo 9.9)" ]] && ok "cache_path empty on miss" || bad "cache_path not empty on miss" srctmp=$(mktemp -d); : > "$srctmp/libfoo-1.2-x86_64-1_danix.txz" cache_store net libfoo "$srctmp/libfoo-1.2-x86_64-1_danix.txz" count=$(find "$CACHE_ROOT/net/libfoo" -name '*.t?z' | wc -l) [[ "$count" -eq 1 ]] && ok "cache_store evicts to one file" || bad "cache_store left $count files" [[ -e "$CACHE_ROOT/net/libfoo/libfoo-1.2-x86_64-1_danix.txz" ]] && ok "cache_store stored new file" || bad "cache_store did not store" rm -rf "$srctmp" # disabled cache -> new CACHE_ROOT_SAVE="$CACHE_ROOT"; USE_CACHE=0 [[ "$(cache_decision net libfoo 1.2)" == "new" ]] && ok "--no-cache disables (new)" || bad "disabled cache got [$(cache_decision net libfoo 1.2)]" USE_CACHE=1; CACHE_ROOT="$CACHE_ROOT_SAVE" # resolve_cache_root prunes stale digests of the SAME variant, keeps the other. # No docker here, so the digest falls back to the tag-derived name; seed sibling # dirs and confirm only the active-variant orphan is removed. PRUNE_CACHE=$(mktemp -d) PKG_CACHE="$PRUNE_CACHE"; ACTIVE_IMAGE="sbo-testbuild:current"; VERSION_ID="current" mkdir -p "$PRUNE_CACHE/current-oldstale" "$PRUNE_CACHE/15.0-keepme" resolve_cache_root [[ -d "$CACHE_ROOT" ]] && ok "cache root created" || bad "cache root missing" [[ ! -e "$PRUNE_CACHE/current-oldstale" ]] && ok "prune drops stale same-variant digest" || bad "stale current digest survived" [[ -d "$PRUNE_CACHE/15.0-keepme" ]] && ok "prune keeps other-variant cache" || bad "15.0 cache wrongly pruned" rm -rf "$PRUNE_CACHE" VERSION_ID="current"; CACHE_ROOT="$CACHE_ROOT_SAVE" # --- BLOCKED-BY-DEP (depends_on_failed) ------------------------------------- dead=(b) if depends_on_failed "$T/cat/g" dead; then ok "g blocked when b dead"; else bad "g should block on b"; fi if depends_on_failed "$T/cat/a" dead; then ok "a blocked when b dead (direct)"; else bad "a should block on b"; fi dead=(c) if depends_on_failed "$T/cat/a" dead; then bad "a wrongly blocked on c"; else ok "a not directly blocked by c"; fi if depends_on_failed "$T/cat/b" dead; then ok "b blocked when c dead"; else bad "b should block on c"; fi dead=() if depends_on_failed "$T/cat/a" dead; then bad "a blocked with empty dead"; else ok "no block when nothing dead"; fi dead=("%README%") if depends_on_failed "$T/cat/a" dead; then bad "%README% treated as dep"; else ok "%README% not treated as dep"; fi echo echo "$pass passed, $fail failed" [[ $fail -eq 0 ]] || exit 1 echo "ALL LOGIC CHECKS PASS"