1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
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"
|