summaryrefslogtreecommitdiffstats
path: root/test-logic.sh
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-07-14 16:59:44 +0200
committerDanilo M. <danix@danix.xyz>2026-07-14 16:59:44 +0200
commit3cd610c212076d438f2d1103e0fc584a310011d5 (patch)
treeb82bd71ff139408dc84f685345862f944653966b /test-logic.sh
downloadsbo-dockerbuild-3cd610c212076d438f2d1103e0fc584a310011d5.tar.gz
sbo-dockerbuild-3cd610c212076d438f2d1103e0fc584a310011d5.zip
initial commit: docker test-build toolchain
Extracted from the .extras/ dir of the sbo-slackbuilds package repo, where it grew from a throwaway helper into a standalone tool. Git history starts fresh; the original 41-commit development log is preserved in HISTORY.md. Contents: the test-build CLI (resolves a local SlackBuild's deps from a configured SBo tree and builds it in a throwaway container, lints, caches deps), the image-builder chain that produces the images it consumes, their pure-logic self-checks, design specs/plans, an install.sh for ~/bin, and docs. Licensed GPLv2-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'test-logic.sh')
-rw-r--r--test-logic.sh227
1 files changed, 227 insertions, 0 deletions
diff --git a/test-logic.sh b/test-logic.sh
new file mode 100644
index 0000000..da50513
--- /dev/null
+++ b/test-logic.sh
@@ -0,0 +1,227 @@
+#!/bin/bash
+#
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+#
+# 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 <prog> "<REQUIRES>".
+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"