| git.druid.rocks | index | druid520 | mp | test/ | suites/ | resolution.sh |
test/suites/resolution.sh
# suite: dependency/tag/slot resolution correctness -- pick_provider
# backtracking, per-package pin/override fallback to a bare name, hard/
# soft/MASK blockers, exact-version pinning, and version comparison.
# a real conflict against an already-installed pkg_slot_use-composed
# package must be detected using the USE-flag-COMPOSED slot, not the raw
# static pkg_slot -- otherwise the conflicting candidate gets picked
# anyway and %tag resolution hard-fails instead of backtracking to the
# next provider.
test_slot_conflict_backtrack() {
t_begin "slot_conflict_backtrack"
reset_state
mkport rt01P <<'EOF'
pkg_name="rt01P"
pkg_ver="1.0"
pkg_tags="rt01tag"
pkg_pref="10"
pkg_slot_use="ssl"
pkg_use="ssl"
install:
touch $MP_PREFIX/rt01P-installed
remove:
rm -f $MP_PREFIX/rt01P-installed
EOF
mkport rt01Q <<'EOF'
pkg_name="rt01Q"
pkg_ver="1.0"
pkg_tags="rt01tag"
pkg_pref="20"
install:
touch $MP_PREFIX/rt01Q-installed
remove:
rm -f $MP_PREFIX/rt01Q-installed
EOF
mkport rt01blocker <<'EOF'
pkg_name="rt01blocker"
pkg_ver="1.0"
pkg_conflicts="rt01P:ssl"
install:
touch $MP_PREFIX/rt01blocker-installed
remove:
rm -f $MP_PREFIX/rt01blocker-installed
EOF
mkport rt01needer <<'EOF'
pkg_name="rt01needer"
pkg_ver="1.0"
pkg_deps="%rt01tag"
install:
touch $MP_PREFIX/rt01needer-installed
remove:
rm -f $MP_PREFIX/rt01needer-installed
EOF
cat >> "$CONF" <<'EOF'
rt01P:
USE=ssl
EOF
mp install rt01blocker >/tmp/rt01.out 2>&1
mp install rt01needer >>/tmp/rt01.out 2>&1
assert_file_absent "$MP_PREFIX/rt01P-installed" "$CUR_TEST" || return
assert_file_exists "$MP_PREFIX/rt01Q-installed" "$CUR_TEST" || return
t_pass
}
# a per-package TARGET_<tag>/DEPS- declared under a port's BARE name must
# still apply to its pkg_slot_use-composed canon, which doesn't exist yet
# at the moment that section is written (portpin/effective_deps' shared
# bare-name fallback, mplib::util::pconf_lookup/pconf_section).
test_barename_pin_applies_to_composed_slot() {
t_begin "barename_pin_applies_to_composed_slot"
reset_state
mkport rt07variant <<'EOF'
pkg_name="rt07variant"
pkg_ver="1.0"
pkg_slot_use="v"
pkg_use="v"
pkg_deps="%rt07tag"
install:
touch $MP_PREFIX/rt07variant-installed
remove:
rm -f $MP_PREFIX/rt07variant-installed
EOF
mkport rt07targetA <<'EOF'
pkg_name="rt07targetA"
pkg_ver="1.0"
pkg_tags="rt07tag"
pkg_pref="10"
install:
touch $MP_PREFIX/rt07targetA-installed
remove:
rm -f $MP_PREFIX/rt07targetA-installed
EOF
mkport rt07targetB <<'EOF'
pkg_name="rt07targetB"
pkg_ver="1.0"
pkg_tags="rt07tag"
pkg_pref="20"
install:
touch $MP_PREFIX/rt07targetB-installed
remove:
rm -f $MP_PREFIX/rt07targetB-installed
EOF
cat >> "$CONF" <<'EOF'
rt07variant:
USE=v
TARGET_rt07tag=rt07targetB
EOF
mp install rt07variant >/tmp/rt07.out 2>&1
assert_file_exists "$MP_PREFIX/rt07targetB-installed" "$CUR_TEST" || return
assert_file_absent "$MP_PREFIX/rt07targetA-installed" "$CUR_TEST" || return
t_pass
}
# blockers: hard pkg_conflicts blocks (and --force overrides), soft
# "!pkg" warns without blocking, MASK=yes excludes a candidate from
# auto-pick and blocks explicit install unless --unmask.
test_blockers_and_mask() {
t_begin "blockers_and_mask"
reset_state
mkport rt08P <<'EOF'
pkg_name="rt08P"
pkg_ver="1.0"
install:
touch $MP_PREFIX/rt08P-installed
remove:
rm -f $MP_PREFIX/rt08P-installed
EOF
mkport rt08hard <<'EOF'
pkg_name="rt08hard"
pkg_ver="1.0"
pkg_conflicts="rt08P"
install:
touch $MP_PREFIX/rt08hard-installed
remove:
rm -f $MP_PREFIX/rt08hard-installed
EOF
mp install rt08P >/tmp/rt08.out 2>&1
mp install rt08hard >>/tmp/rt08.out 2>&1
assert_file_absent "$MP_PREFIX/rt08hard-installed" "$CUR_TEST" || return
mp install --force rt08hard >>/tmp/rt08.out 2>&1
assert_file_exists "$MP_PREFIX/rt08hard-installed" "$CUR_TEST" || return
mkport rt08masked <<'EOF'
pkg_name="rt08masked"
pkg_ver="1.0"
install:
touch $MP_PREFIX/rt08masked-installed
remove:
rm -f $MP_PREFIX/rt08masked-installed
EOF
cat >> "$CONF" <<'EOF'
rt08masked:
MASK=yes
EOF
out=$(mp install rt08masked 2>&1)
assert_contains "$out" "masked" "$CUR_TEST" || return
assert_file_absent "$MP_PREFIX/rt08masked-installed" "$CUR_TEST" || return
mp install --unmask rt08masked >/tmp/rt08c.out 2>&1
assert_file_exists "$MP_PREFIX/rt08masked-installed" "$CUR_TEST" || return
t_pass
}
# version pinning: TARGET_<tag>=name=version pins an exact version;
# already-installed-at-a-different-version fails loudly instead of
# silently keeping the mismatched version.
test_version_pin() {
t_begin "version_pin"
reset_state
mkport rt09prov <<'EOF'
pkg_name="rt09prov"
pkg_ver="9.9.9"
pkg_tags="rt09tag"
install:
touch $MP_PREFIX/rt09prov-installed
remove:
rm -f $MP_PREFIX/rt09prov-installed
EOF
mkport rt09consumerA <<'EOF'
pkg_name="rt09consumerA"
pkg_ver="1.0"
pkg_deps="%rt09tag"
install:
touch $MP_PREFIX/rt09consumerA-installed
remove:
rm -f $MP_PREFIX/rt09consumerA-installed
EOF
mkport rt09consumerB <<'EOF'
pkg_name="rt09consumerB"
pkg_ver="1.0"
pkg_deps="%rt09tag"
install:
touch $MP_PREFIX/rt09consumerB-installed
remove:
rm -f $MP_PREFIX/rt09consumerB-installed
EOF
echo 'TARGET_rt09tag=rt09prov=1.1.1' >> "$CONF"
mp install rt09consumerA >/tmp/rt09.out 2>&1
db=$(cat "$WD/db")
assert_contains "$db" "rt09prov 1.1.1" "$CUR_TEST" || return
sed -i 's/TARGET_rt09tag=rt09prov=1.1.1/TARGET_rt09tag=rt09prov=2.0.0/' "$CONF"
out=$(mp install rt09consumerB 2>&1)
assert_contains "$out" "does not satisfy" "$CUR_TEST" || return
assert_file_absent "$MP_PREFIX/rt09consumerB-installed" "$CUR_TEST" || return
t_pass
}
# providers.pl must recognize a TARGET_<tag> pin, not just TAG_<tag>.
test_providers_target_pin() {
t_begin "providers_target_pin"
reset_state
echo 'TARGET_rt17tag=rt17someport' >> "$CONF"
out=$(mp providers rt17tag 2>&1)
assert_contains "$out" "rt17someport" "$CUR_TEST" || return
assert_contains "$out" "pinned" "$CUR_TEST" || return
t_pass
}
# mplib::version's pre-release stage ordering must be alpha < beta < pre
# < rc < release (the FreeBSD/pkgsrc convention the code documents
# following), not pre/rc sorting before alpha/beta.
test_version_stage_ordering() {
t_begin "version_stage_ordering"
reset_state
out=$(cd /root/mpbuildtest && perl -I. -Implib -Mmplib::version=vercmp -e '
print "rc_vs_b:", vercmp("1.0rc1","1.0b2"), "\n";
print "a_vs_b:", vercmp("1.0a1","1.0b1"), "\n";
print "pre_vs_rc:", vercmp("1.0pre1","1.0rc1"), "\n";
print "rc_vs_release:", vercmp("1.0rc1","1.0"), "\n";
')
assert_contains "$out" "rc_vs_b:1" "$CUR_TEST" || return
assert_contains "$out" "a_vs_b:-1" "$CUR_TEST" || return
assert_contains "$out" "pre_vs_rc:-1" "$CUR_TEST" || return
assert_contains "$out" "rc_vs_release:-1" "$CUR_TEST" || return
t_pass
}
# a MASK=yes section on the lowest-pref provider must remove it from
# %tag auto-pick entirely, handing the pick to the next provider.
test_masked_provider_skip() {
t_begin "masked_provider_skip"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkport rt24p1 <<'EOF'
pkg_name="rt24p1"
pkg_ver="1.0"
pkg_tags="rt24tag"
pkg_pref="10"
install:
touch $MP_PREFIX/rt24p1-installed
remove:
rm -f $MP_PREFIX/rt24p1-installed
EOF
mkport rt24p2 <<'EOF'
pkg_name="rt24p2"
pkg_ver="1.0"
pkg_tags="rt24tag"
pkg_pref="20"
install:
touch $MP_PREFIX/rt24p2-installed
remove:
rm -f $MP_PREFIX/rt24p2-installed
EOF
mkport rt24need <<'EOF'
pkg_name="rt24need"
pkg_ver="1.0"
pkg_deps="%rt24tag"
install:
touch $MP_PREFIX/rt24need-installed
remove:
rm -f $MP_PREFIX/rt24need-installed
EOF
cat >> "$CONF" <<'EOF'
rt24p1:
MASK=yes
EOF
mp install rt24need >/tmp/rt24.out 2>&1
assert_file_exists "$MP_PREFIX/rt24p2-installed" "$CUR_TEST" || return
assert_file_absent "$MP_PREFIX/rt24p1-installed" "$CUR_TEST" || return
t_pass
}
# a soft "!pkg" conflict warns (both directions) but never blocks the
# install.
test_soft_conflict_warns() {
t_begin "soft_conflict_warns"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkport rt25b <<'EOF'
pkg_name="rt25b"
pkg_ver="1.0"
install:
touch $MP_PREFIX/rt25b-installed
remove:
rm -f $MP_PREFIX/rt25b-installed
EOF
mkport rt25a <<'EOF'
pkg_name="rt25a"
pkg_ver="1.0"
pkg_conflicts="!rt25b"
install:
touch $MP_PREFIX/rt25a-installed
remove:
rm -f $MP_PREFIX/rt25a-installed
EOF
mp install rt25b >/tmp/rt25.out 2>&1
out=$(mp install rt25a 2>&1)
rc=$?
assert_exit0 "$rc" "$CUR_TEST" || return
assert_contains "$out" "softly conflicts" "$CUR_TEST" || return
assert_file_exists "$MP_PREFIX/rt25a-installed" "$CUR_TEST" || return
t_pass
}
# transactional soft-conflict resolution (mplib::resolve::install_pkg_
# body, soft_conflict_targets + reachable_needed_set): a soft conflict
# with an installed package NOTHING needs at all (never requested,
# unreachable from anything that is) gets auto-removed as part of the
# new install, instead of sitting there uselessly alongside a warning
# forever the way test_soft_conflict_warns' own scenario (a DIRECTLY
# requested conflicting package) still correctly does.
test_soft_conflict_auto_removes_unneeded() {
t_begin "soft_conflict_auto_removes_unneeded"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkport rt45a <<'EOF'
pkg_name="rt45a"
pkg_ver="1.0"
install:
touch $MP_PREFIX/rt45a-installed
remove:
rm -f $MP_PREFIX/rt45a-installed
EOF
mkport rt45consumer <<'EOF'
pkg_name="rt45consumer"
pkg_ver="1.0"
pkg_deps="rt45a"
install:
touch $MP_PREFIX/rt45consumer-installed
remove:
rm -f $MP_PREFIX/rt45consumer-installed
EOF
mkport rt45new <<'EOF'
pkg_name="rt45new"
pkg_ver="1.0"
pkg_conflicts="!rt45a"
install:
touch $MP_PREFIX/rt45new-installed
remove:
rm -f $MP_PREFIX/rt45new-installed
EOF
mp install rt45consumer >/tmp/rt45.out 2>&1
mp remove --force rt45consumer >>/tmp/rt45.out 2>&1
# rt45a is now an orphan: was never requested itself, and nothing
# reachable from any requested package needs it anymore.
assert_file_exists "$MP_PREFIX/rt45a-installed" "$CUR_TEST" || return
out=$(mp install rt45new 2>&1)
rc=$?
assert_exit0 "$rc" "$CUR_TEST" || return
assert_file_exists "$MP_PREFIX/rt45new-installed" "$CUR_TEST" || return
assert_file_absent "$MP_PREFIX/rt45a-installed" "$CUR_TEST" || return
t_pass
}
# same mechanism, opposite outcome: the softly-conflicting installed
# package is NOT directly requested, but IS still reachable as some
# OTHER requested package's own runtime dependency -- must be left
# alone (neither removed nor a hard failure), proving reachable_needed_
# set's own transitive reachability (not just the bare "requested"
# flag test_soft_conflict_warns' own scenario already covers) is what
# actually gates this.
test_soft_conflict_leaves_transitively_needed_alone() {
t_begin "soft_conflict_leaves_transitively_needed_alone"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkport rt46b <<'EOF'
pkg_name="rt46b"
pkg_ver="1.0"
install:
touch $MP_PREFIX/rt46b-installed
remove:
rm -f $MP_PREFIX/rt46b-installed
EOF
mkport rt46other <<'EOF'
pkg_name="rt46other"
pkg_ver="1.0"
pkg_deps="rt46b"
install:
touch $MP_PREFIX/rt46other-installed
remove:
rm -f $MP_PREFIX/rt46other-installed
EOF
mkport rt46new <<'EOF'
pkg_name="rt46new"
pkg_ver="1.0"
pkg_conflicts="!rt46b"
install:
touch $MP_PREFIX/rt46new-installed
remove:
rm -f $MP_PREFIX/rt46new-installed
EOF
mp install rt46other >/tmp/rt46.out 2>&1
out=$(mp install rt46new 2>&1)
rc=$?
assert_exit0 "$rc" "$CUR_TEST" || return
assert_contains "$out" "softly conflicts" "$CUR_TEST" || return
assert_file_exists "$MP_PREFIX/rt46new-installed" "$CUR_TEST" || return
assert_file_exists "$MP_PREFIX/rt46b-installed" "$CUR_TEST" || return
assert_file_exists "$MP_PREFIX/rt46other-installed" "$CUR_TEST" || return
t_pass
}
# slot operators / subslot rebuild cascade ("@dep" in pkg_deps=,
# mplib::resolve's own record_subslot_dep/subslot_changed_targets,
# update.pl's own rebuild-cascade block): a "@"-marked dependency records
# what pkg_subslot the provider had at build time (subslots.db). this
# proves the basic recording half works: installing a consumer that
# depends on "@rt60prov" leaves exactly one subslots.db line naming the
# provider's CURRENT pkg_subslot.
test_subslot_dep_recorded_on_install() {
t_begin "subslot_dep_recorded_on_install"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkport rt60prov <<'EOF'
pkg_name="rt60prov"
pkg_ver="1.0"
pkg_subslot="1"
install:
touch $MP_PREFIX/rt60prov-installed
remove:
rm -f $MP_PREFIX/rt60prov-installed
EOF
mkport rt60cons <<'EOF'
pkg_name="rt60cons"
pkg_ver="1.0"
pkg_deps="@rt60prov"
install:
touch $MP_PREFIX/rt60cons-installed
remove:
rm -f $MP_PREFIX/rt60cons-installed
EOF
out=$(mp install rt60cons 2>&1)
rc=$?
assert_exit0 "$rc" "$CUR_TEST" || return
assert_file_exists "$MP_PREFIX/rt60cons-installed" "$CUR_TEST" || return
assert_contains "$(cat "$WD/subslots.db" 2>/dev/null)" "rt60cons rt60prov 1" "$CUR_TEST" || return
t_pass
}
# the actual rebuild-cascade half: once rt60prov's OWN pkg_subslot moves
# (1 -> 2), a narrower "mp update rt60prov" -- naming only the provider,
# never mentioning rt60cons at all -- must still pull rt60cons into the
# same update batch and actually rebuild it (Portage's own ":=" slot
# operator, in spirit: an ABI-relevant provider change cascades to every
# dependent that tracked it, without the caller having to know or name
# them). asserts on a version-bumped port's own fresh marker file, not
# just "still present", so a stale pre-cascade install can't false-pass.
test_subslot_change_triggers_dependent_rebuild() {
t_begin "subslot_change_triggers_dependent_rebuild"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkport rt61prov <<'EOF'
pkg_name="rt61prov"
pkg_ver="1.0"
pkg_subslot="1"
install:
touch $MP_PREFIX/rt61prov-installed
remove:
rm -f $MP_PREFIX/rt61prov-installed
EOF
mkport rt61cons <<'EOF'
pkg_name="rt61cons"
pkg_ver="1.0"
pkg_deps="@rt61prov"
install:
touch $MP_PREFIX/rt61cons-installed
remove:
rm -f $MP_PREFIX/rt61cons-installed
EOF
mp install rt61cons >/tmp/rt61.out 2>&1
mkport rt61prov <<'EOF'
pkg_name="rt61prov"
pkg_ver="1.1"
pkg_subslot="2"
install:
touch $MP_PREFIX/rt61prov-installed
remove:
rm -f $MP_PREFIX/rt61prov-installed
EOF
mkport rt61cons <<'EOF'
pkg_name="rt61cons"
pkg_ver="1.1"
pkg_deps="@rt61prov"
install:
touch $MP_PREFIX/rt61cons-installed
touch $MP_PREFIX/rt61cons-installed-v2
remove:
rm -f $MP_PREFIX/rt61cons-installed $MP_PREFIX/rt61cons-installed-v2
EOF
out=$(mp update rt61prov 2>&1)
rc=$?
assert_exit0 "$rc" "$CUR_TEST" || return
assert_file_exists "$MP_PREFIX/rt61cons-installed-v2" "$CUR_TEST" || return
t_pass
}
# when every %tag provider conflicts with something already installed,
# non-interactive resolution must fail loudly (no tty to prompt on)
# rather than picking a conflicting one anyway.
test_provider_exhausted_fails() {
t_begin "provider_exhausted_fails"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkport rt26x <<'EOF'
pkg_name="rt26x"
pkg_ver="1.0"
install:
touch $MP_PREFIX/rt26x-installed
remove:
rm -f $MP_PREFIX/rt26x-installed
EOF
mkport rt26p1 <<'EOF'
pkg_name="rt26p1"
pkg_ver="1.0"
pkg_tags="rt26tag"
pkg_pref="10"
pkg_conflicts="rt26x"
install:
touch $MP_PREFIX/rt26p1-installed
remove:
rm -f $MP_PREFIX/rt26p1-installed
EOF
mkport rt26p2 <<'EOF'
pkg_name="rt26p2"
pkg_ver="1.0"
pkg_tags="rt26tag"
pkg_pref="20"
pkg_conflicts="rt26x"
install:
touch $MP_PREFIX/rt26p2-installed
remove:
rm -f $MP_PREFIX/rt26p2-installed
EOF
mkport rt26need <<'EOF'
pkg_name="rt26need"
pkg_ver="1.0"
pkg_deps="%rt26tag"
install:
touch $MP_PREFIX/rt26need-installed
remove:
rm -f $MP_PREFIX/rt26need-installed
EOF
mp install rt26x >/tmp/rt26.out 2>&1
out=$(mp install rt26need 2>&1)
rc=$?
assert_exit_nonzero "$rc" "$CUR_TEST" || return
assert_contains "$out" "without conflicting" "$CUR_TEST" || return
assert_file_absent "$MP_PREFIX/rt26need-installed" "$CUR_TEST" || return
assert_file_absent "$MP_PREFIX/rt26p1-installed" "$CUR_TEST" || return
assert_file_absent "$MP_PREFIX/rt26p2-installed" "$CUR_TEST" || return
t_pass
}
# among many providers of one tag, the lowest pkg_pref wins the auto-pick.
test_fanout_lowest_pref() {
t_begin "fanout_lowest_pref"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
i=1
while [ "$i" -le 6 ]; do
mkport "rt27p$i" <<EOF
pkg_name="rt27p$i"
pkg_ver="1.0"
pkg_tags="rt27tag"
pkg_pref="$((i * 10))"
install:
touch \$MP_PREFIX/rt27p$i-installed
remove:
rm -f \$MP_PREFIX/rt27p$i-installed
EOF
i=$((i + 1))
done
mkport rt27need <<'EOF'
pkg_name="rt27need"
pkg_ver="1.0"
pkg_deps="%rt27tag"
install:
touch $MP_PREFIX/rt27need-installed
remove:
rm -f $MP_PREFIX/rt27need-installed
EOF
mp install rt27need >/tmp/rt27.out 2>&1
assert_file_exists "$MP_PREFIX/rt27p1-installed" "$CUR_TEST" || return
i=2
while [ "$i" -le 6 ]; do
assert_file_absent "$MP_PREFIX/rt27p$i-installed" "$CUR_TEST" || return
i=$((i + 1))
done
t_pass
}
# shared setup for the provider-selection matrix below: two same-name
# providers (static slots a/b) of one tag plus a consumer, with $conf
# appended to mp.conf.
prov_matrix_setup() {
# $1 name
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkport "$1"pa <<EOF
pkg_name="$1p"
pkg_ver="1.0"
pkg_slot="a"
pkg_tags="$1tag"
pkg_pref="10"
install:
touch \$MP_PREFIX/$1pa-installed
remove:
rm -f \$MP_PREFIX/$1pa-installed
EOF
mkport "$1"pb <<EOF
pkg_name="$1p"
pkg_ver="1.0"
pkg_slot="b"
pkg_tags="$1tag"
pkg_pref="20"
install:
touch \$MP_PREFIX/$1pb-installed
remove:
rm -f \$MP_PREFIX/$1pb-installed
EOF
mkport "$1"need <<EOF
pkg_name="$1need"
pkg_ver="1.0"
pkg_deps="%$1tag"
install:
touch \$MP_PREFIX/$1need-installed
remove:
rm -f \$MP_PREFIX/$1need-installed
EOF
}
# a TAG_<tag> pin (the fallback key) is honored exactly like TARGET_<tag>.
test_tag_key_pin() {
t_begin "tag_key_pin"
reset_state
prov_matrix_setup rt30
echo 'TAG_rt30tag=rt30pb' >> "$CONF"
mp install rt30need >/tmp/rt30.out 2>&1
assert_file_exists "$MP_PREFIX/rt30p-b/rt30pb-installed" "$CUR_TEST" || return
assert_file_absent "$MP_PREFIX/rt30p-a/rt30pa-installed" "$CUR_TEST" || return
t_pass
}
# when both TARGET_ and TAG_ set the same tag, TARGET_ wins.
test_target_beats_tag_key() {
t_begin "target_beats_tag_key"
reset_state
prov_matrix_setup rt31
echo 'TAG_rt31tag=rt31pa' >> "$CONF"
echo 'TARGET_rt31tag=rt31pb' >> "$CONF"
mp install rt31need >/tmp/rt31.out 2>&1
assert_file_exists "$MP_PREFIX/rt31p-b/rt31pb-installed" "$CUR_TEST" || return
assert_file_absent "$MP_PREFIX/rt31p-a/rt31pa-installed" "$CUR_TEST" || return
t_pass
}
# a per-package TARGET_ pin on the consumer beats a global TAG_ pin.
test_perpkg_pin_beats_global() {
t_begin "perpkg_pin_beats_global"
reset_state
prov_matrix_setup rt32
echo 'TAG_rt32tag=rt32pa' >> "$CONF"
cat >> "$CONF" <<'EOF'
rt32need:
TARGET_rt32tag=rt32pb
EOF
mp install rt32need >/tmp/rt32.out 2>&1
assert_file_exists "$MP_PREFIX/rt32p-b/rt32pb-installed" "$CUR_TEST" || return
assert_file_absent "$MP_PREFIX/rt32p-a/rt32pa-installed" "$CUR_TEST" || return
t_pass
}
# an already-installed provider short-circuits resolution even though it
# is not the lowest pref.
test_installed_provider_shortcut() {
t_begin "installed_provider_shortcut"
reset_state
prov_matrix_setup rt33
mp install rt33pb >/tmp/rt33.out 2>&1
mp install rt33need >>/tmp/rt33.out 2>&1
assert_file_exists "$MP_PREFIX/rt33p-b/rt33pb-installed" "$CUR_TEST" || return
assert_file_absent "$MP_PREFIX/rt33p-a/rt33pa-installed" "$CUR_TEST" || return
t_pass
}
# a soft "!consumer" conflict on the preferred provider warns but does
# not remove it from consideration -- it still wins the pick.
test_soft_conflict_provider_still_picked() {
t_begin "soft_conflict_provider_still_picked"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkport rt34x <<'EOF'
pkg_name="rt34x"
pkg_ver="1.0"
install:
touch $MP_PREFIX/rt34x-installed
remove:
rm -f $MP_PREFIX/rt34x-installed
EOF
mkport rt34p1 <<'EOF'
pkg_name="rt34p"
pkg_ver="1.0"
pkg_slot="a"
pkg_tags="rt34tag"
pkg_pref="10"
pkg_conflicts="!rt34x"
install:
touch $MP_PREFIX/rt34p1-installed
remove:
rm -f $MP_PREFIX/rt34p1-installed
EOF
mkport rt34p2 <<'EOF'
pkg_name="rt34p"
pkg_ver="1.0"
pkg_slot="b"
pkg_tags="rt34tag"
pkg_pref="20"
install:
touch $MP_PREFIX/rt34p2-installed
remove:
rm -f $MP_PREFIX/rt34p2-installed
EOF
mkport rt34need <<'EOF'
pkg_name="rt34need"
pkg_ver="1.0"
pkg_deps="%rt34tag"
install:
touch $MP_PREFIX/rt34need-installed
remove:
rm -f $MP_PREFIX/rt34need-installed
EOF
mp install rt34x >/tmp/rt34.out 2>&1
out=$(mp install rt34need 2>&1)
rc=$?
assert_exit0 "$rc" "$CUR_TEST" || return
assert_contains "$out" "softly conflicts" "$CUR_TEST" || return
assert_file_exists "$MP_PREFIX/rt34p-a/rt34p1-installed" "$CUR_TEST" || return
assert_file_absent "$MP_PREFIX/rt34p-b/rt34p2-installed" "$CUR_TEST" || return
t_pass
}
# a "!!" hard blocker on an installed package blocks both the provider's
# auto-pick and any explicit install, even with --force only as the
# documented override.
test_double_bang_blocker() {
t_begin "double_bang_blocker"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkport rt35y <<'EOF'
pkg_name="rt35y"
pkg_ver="1.0"
install:
touch $MP_PREFIX/rt35y-installed
remove:
rm -f $MP_PREFIX/rt35y-installed
EOF
mkport rt35z <<'EOF'
pkg_name="rt35z"
pkg_ver="1.0"
pkg_conflicts="!!rt35y"
install:
touch $MP_PREFIX/rt35z-installed
remove:
rm -f $MP_PREFIX/rt35z-installed
EOF
mp install rt35y >/tmp/rt35.out 2>&1
out=$(mp install rt35z 2>&1)
rc=$?
assert_exit_nonzero "$rc" "$CUR_TEST" || return
assert_contains "$out" "conflicts" "$CUR_TEST" || return
assert_file_absent "$MP_PREFIX/rt35z-installed" "$CUR_TEST" || return
mp install --force rt35z >>/tmp/rt35.out 2>&1
assert_file_exists "$MP_PREFIX/rt35z-installed" "$CUR_TEST" || return
t_pass
}
# a provider that conflicts with an already-installed SIBLING provider of
# the same tag is passed over: the installed one wins the pick outright.
test_provider_vs_provider_conflict() {
t_begin "provider_vs_provider_conflict"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkport rt36t1a <<'EOF'
pkg_name="rt36t1a"
pkg_ver="1.0"
pkg_tags="rt36tag1"
pkg_pref="10"
install:
touch $MP_PREFIX/rt36t1a-installed
remove:
rm -f $MP_PREFIX/rt36t1a-installed
EOF
mkport rt36t2a <<'EOF'
pkg_name="rt36t2a"
pkg_ver="1.0"
pkg_tags="rt36tag2"
pkg_pref="10"
pkg_conflicts="rt36t2b"
install:
touch $MP_PREFIX/rt36t2a-installed
remove:
rm -f $MP_PREFIX/rt36t2a-installed
EOF
mkport rt36t2b <<'EOF'
pkg_name="rt36t2b"
pkg_ver="1.0"
pkg_tags="rt36tag2"
pkg_pref="20"
install:
touch $MP_PREFIX/rt36t2b-installed
remove:
rm -f $MP_PREFIX/rt36t2b-installed
EOF
mkport rt36need <<'EOF'
pkg_name="rt36need"
pkg_ver="1.0"
pkg_deps="%rt36tag1 %rt36tag2"
install:
touch $MP_PREFIX/rt36need-installed
remove:
rm -f $MP_PREFIX/rt36need-installed
EOF
mp install rt36t2b >/tmp/rt36.out 2>&1
mp install rt36need >>/tmp/rt36.out 2>&1
rc=$?
assert_exit0 "$rc" "$CUR_TEST" || return
assert_file_exists "$MP_PREFIX/rt36t1a-installed" "$CUR_TEST" || return
assert_file_exists "$MP_PREFIX/rt36t2b-installed" "$CUR_TEST" || return
assert_file_absent "$MP_PREFIX/rt36t2a-installed" "$CUR_TEST" || return
t_pass
}
# a MASK=yes on a %tag provider blocks its explicit install AND removes
# it from auto-pick; --unmask overrides only the explicit-install block.
test_mask_provider_explicit_and_pick() {
t_begin "mask_provider_explicit_and_pick"
reset_state
prov_matrix_setup rt37
cat >> "$CONF" <<'EOF'
rt37p:a:
MASK=yes
EOF
out=$(mp install rt37p:a 2>&1)
rc=$?
assert_exit_nonzero "$rc" "$CUR_TEST" || return
assert_contains "$out" "masked" "$CUR_TEST" || return
mp install rt37need >/tmp/rt37.out 2>&1
assert_file_exists "$MP_PREFIX/rt37p-b/rt37pb-installed" "$CUR_TEST" || return
assert_file_absent "$MP_PREFIX/rt37p-a/rt37pa-installed" "$CUR_TEST" || return
mp install --unmask rt37p:a >>/tmp/rt37.out 2>&1
assert_file_exists "$MP_PREFIX/rt37p-a/rt37pa-installed" "$CUR_TEST" || return
t_pass
}
# a ten-provider fanout still collapses to the single lowest pref, and
# the db records exactly one provider line for the tag.
test_deep_fanout_single_pick() {
t_begin "deep_fanout_single_pick"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
i=1
while [ "$i" -le 10 ]; do
mkport "rt38p$i" <<EOF
pkg_name="rt38p$i"
pkg_ver="1.0"
pkg_tags="rt38tag"
pkg_pref="$((i * 5))"
install:
touch \$MP_PREFIX/rt38p$i-installed
remove:
rm -f \$MP_PREFIX/rt38p$i-installed
EOF
i=$((i + 1))
done
mkport rt38need <<'EOF'
pkg_name="rt38need"
pkg_ver="1.0"
pkg_deps="%rt38tag"
install:
touch $MP_PREFIX/rt38need-installed
remove:
rm -f $MP_PREFIX/rt38need-installed
EOF
mp install rt38need >/tmp/rt38.out 2>&1
assert_file_exists "$MP_PREFIX/rt38p1-installed" "$CUR_TEST" || return
i=2
while [ "$i" -le 10 ]; do
assert_file_absent "$MP_PREFIX/rt38p$i-installed" "$CUR_TEST" || return
i=$((i + 1))
done
t_pass
}
# a pin naming a nonexistent provider fails loudly instead of falling
# back to auto-pick.
test_pin_to_missing_provider_fails() {
t_begin "pin_to_missing_provider_fails"
reset_state
prov_matrix_setup rt39
echo 'TARGET_rt39tag=nosuchrt39provider' >> "$CONF"
out=$(mp install rt39need 2>&1)
rc=$?
assert_exit_nonzero "$rc" "$CUR_TEST" || return
assert_contains "$out" "not found" "$CUR_TEST" || return
assert_file_absent "$MP_PREFIX/rt39need-installed" "$CUR_TEST" || return
t_pass
}
# CONFLICTS= (mp.conf, per-package): a manual override on top of a
# port's own declared pkg_conflicts, same +/- delta convention as
# USE=/"mp use" -- a raw "-token" delta lifts a declared hard blocker
# without --force, a raw bare list adds a conflict a port never declared
# at all, and "mp conflicts" (the command wrapper) round-trips the same
# thing including the empty/"-none-" case.
test_conflicts_override() {
t_begin "conflicts_override"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkport rt60a <<'EOF'
pkg_name="rt60a"
pkg_ver="1.0"
install:
touch $MP_PREFIX/rt60a-installed
remove:
rm -f $MP_PREFIX/rt60a-installed
EOF
mkport rt60b <<'EOF'
pkg_name="rt60b"
pkg_ver="1.0"
pkg_conflicts="rt60a"
install:
touch $MP_PREFIX/rt60b-installed
remove:
rm -f $MP_PREFIX/rt60b-installed
EOF
mkport rt60c <<'EOF'
pkg_name="rt60c"
pkg_ver="1.0"
install:
touch $MP_PREFIX/rt60c-installed
remove:
rm -f $MP_PREFIX/rt60c-installed
EOF
mp install rt60a >/tmp/rt60.out 2>&1
# "mp conflicts" read-only listing, checked before any override is
# applied, shows rt60b's own declared conflict.
out=$(mp conflicts rt60b 2>&1)
assert_contains "$out" "rt60a" "$CUR_TEST (listing shows declared conflict)" || return
# baseline: rt60b's own declared hard conflict blocks without --force.
mp install rt60b >>/tmp/rt60.out 2>&1
assert_file_absent "$MP_PREFIX/rt60b-installed" "$CUR_TEST (baseline block)" || return
# raw CONFLICTS= delta lifts the declared conflict by hand-editing
# mp.conf directly, no command involved.
cat >> "$CONF" <<'EOF'
rt60b:
CONFLICTS=-rt60a
EOF
mp install rt60b >>/tmp/rt60.out 2>&1
assert_file_exists "$MP_PREFIX/rt60b-installed" "$CUR_TEST (raw delta lifts conflict)" || return
mp remove --force rt60b >/dev/null 2>&1
# raw CONFLICTS= bare list adds a conflict rt60c never declared.
cat >> "$CONF" <<'EOF'
rt60c:
CONFLICTS=rt60a
EOF
mp install rt60c >>/tmp/rt60.out 2>&1
assert_file_absent "$MP_PREFIX/rt60c-installed" "$CUR_TEST (raw literal adds conflict)" || return
mp install --force rt60c >>/tmp/rt60.out 2>&1
assert_file_exists "$MP_PREFIX/rt60c-installed" "$CUR_TEST (--force still overrides)" || return
mp remove --force rt60c >/dev/null 2>&1
# "mp conflicts" command: +/- toggle and the empty/"-none-"
# round-trip, mirroring "mp use" exactly.
mp conflicts rt60b -rt60a >/tmp/rt60.out 2>&1
rc=$?
assert_exit0 "$rc" "$CUR_TEST (toggle command exit)" || return
mp install rt60b >>/tmp/rt60.out 2>&1
assert_file_exists "$MP_PREFIX/rt60b-installed" "$CUR_TEST (command-persisted delta lifts conflict)" || return
out=$(mp conflicts rt60b 2>&1)
rc=$?
assert_exit_nonzero "$rc" "$CUR_TEST (empty listing fails loudly)" || return
assert_contains "$out" "no conflicts" "$CUR_TEST (empty listing message)" || return
t_pass
}
# real file-path conflicts (find_file_conflict, NOT a declared
# pkg_conflicts relationship -- both rt70a and rt70b below install to
# the exact same path with no pkg_conflicts= at all) -- baseline: with
# neither side declaring CONFLICT_PRIORITY, the second install fails
# exactly like the pre-existing hard-fail path always has, and the
# first installer's file is left in place, untouched.
test_file_conflict_no_priority_fails() {
t_begin "file_conflict_no_priority_fails"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkport rt70a <<'EOF'
pkg_name="rt70a"
pkg_ver="1.0"
install:
echo a > $MP_PREFIX/rt70-shared
remove:
rm -f $MP_PREFIX/rt70-shared
EOF
mkport rt70b <<'EOF'
pkg_name="rt70b"
pkg_ver="1.0"
install:
echo b > $MP_PREFIX/rt70-shared
remove:
rm -f $MP_PREFIX/rt70-shared
EOF
mp install rt70a >/tmp/rt70.out 2>&1
out=$(mp install rt70b 2>&1)
rc=$?
assert_exit_nonzero "$rc" "$CUR_TEST" || return
assert_contains "$out" "conflicts with rt70a on installed files" "$CUR_TEST" || return
# NOT asserting the file's own content here: install_pkg_body's own
# documented tradeoff is that a rejected install's phase scripts
# already ran (and so already wrote rt70b's own "b") BEFORE the
# post-install conflict check ever runs -- only $canon's own stage
# dir gets rolled back, never files already written to the live
# prefix (see install_pkg_body's own comment on why: blindly
# running rt70b's remove.sh to "undo" it risks deleting rt70a's own
# real file instead of rt70b's copy of the same path). rt70a's own
# manifest still correctly lists it either way -- that's the
# property that actually matters and stays checked below.
out=$(mp owns "$MP_PREFIX/rt70-shared" 2>&1)
assert_contains "$out" "owned by rt70a" "$CUR_TEST (rt70a's own manifest still claims it)" || return
t_pass
}
# CONFLICT_PRIORITY (mp.conf's own per-package key) resolves the exact
# same collision automatically: the higher-priority side wins, and the
# loser's own manifest is updated to stop claiming the path that
# changed hands -- checked via "mp owns", not just that the install
# succeeded, since a stale claim in the loser's own manifest is
# exactly the class of drift this feature exists to prevent.
test_file_conflict_priority_auto_resolves() {
t_begin "file_conflict_priority_auto_resolves"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkport rt71a <<'EOF'
pkg_name="rt71a"
pkg_ver="1.0"
install:
echo a > $MP_PREFIX/rt71-shared
remove:
rm -f $MP_PREFIX/rt71-shared
EOF
mkport rt71b <<'EOF'
pkg_name="rt71b"
pkg_ver="1.0"
install:
echo b > $MP_PREFIX/rt71-shared
remove:
rm -f $MP_PREFIX/rt71-shared
EOF
cat >> "$CONF" <<'EOF'
rt71a:
CONFLICT_PRIORITY=10
rt71b:
CONFLICT_PRIORITY=100
EOF
mp install rt71a >/tmp/rt71.out 2>&1
out=$(mp install rt71b 2>&1)
rc=$?
assert_exit0 "$rc" "$CUR_TEST (higher-priority side installs clean)" || return
assert_contains "$(cat "$MP_PREFIX/rt71-shared")" "b" "$CUR_TEST (winner's own file content)" || return
out=$(mp owns "$MP_PREFIX/rt71-shared" 2>&1)
assert_contains "$out" "owned by rt71b" "$CUR_TEST (mp owns reflects the new owner)" || return
assert_not_contains "$out" "owned by rt71a" "$CUR_TEST (loser's own manifest no longer claims it)" || return
t_pass
}
# same pair, reversed: the LOWER-priority side tries to take a file the
# HIGHER-priority side already owns. BUG fixed: this used to hard-fail
# the whole install (contradicting CONFLICT_PRIORITY's own documented
# promise) -- now the loser's own install still succeeds overall (this
# ONE file just isn't part of its manifest), and the winner's own
# legitimate content is restored (it got overwritten once, by the
# loser's own install: phase, before the conflict check could run).
test_file_conflict_priority_loser_excluded() {
t_begin "file_conflict_priority_loser_excluded"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkport rt72a <<'EOF'
pkg_name="rt72a"
pkg_ver="1.0"
install:
echo a > $MP_PREFIX/rt72-shared
remove:
rm -f $MP_PREFIX/rt72-shared
EOF
mkport rt72b <<'EOF'
pkg_name="rt72b"
pkg_ver="1.0"
install:
echo b > $MP_PREFIX/rt72-shared
echo ownfile > $MP_PREFIX/rt72b-own-file
remove:
rm -f $MP_PREFIX/rt72-shared $MP_PREFIX/rt72b-own-file
EOF
cat >> "$CONF" <<'EOF'
rt72a:
CONFLICT_PRIORITY=100
rt72b:
CONFLICT_PRIORITY=10
EOF
mp install rt72a >/tmp/rt72.out 2>&1
out=$(mp install rt72b 2>&1)
rc=$?
assert_exit0 "$rc" "$CUR_TEST (loser's own install still succeeds overall)" || return
assert_contains "$out" "CONFLICT_PRIORITY, repairing rt72a" "$CUR_TEST" || return
assert_contains "$(cat "$MP_PREFIX/rt72-shared")" "a" "$CUR_TEST (winner's own real content restored)" || return
assert_file_exists "$MP_PREFIX/rt72b-own-file" "$CUR_TEST (loser's own non-conflicting file still installed)" || return
out=$(mp owns "$MP_PREFIX/rt72-shared" 2>&1)
assert_contains "$out" "owned by rt72a" "$CUR_TEST (winner's own manifest still claims it)" || return
assert_not_contains "$out" "owned by rt72b" "$CUR_TEST (loser's own manifest excludes it)" || return
out=$(mp owns "$MP_PREFIX/rt72b-own-file" 2>&1)
assert_contains "$out" "owned by rt72b" "$CUR_TEST (loser still owns its own non-conflicting file)" || return
t_pass
}
# the fast pre-flight check (install_pkg_body, right alongside
# check_static_conflicts -- runs BEFORE any phase script at all): a
# package whose own LAST-KNOWN manifest already conflicts with an
# installed higher-priority package is rejected before its own
# install: phase ever runs, not just eventually via the authoritative
# post-install check. proven by a marker file install: would write
# that never appears -- if the pre-flight didn't fire, install: would
# still run (and lose at the authoritative check afterward, but only
# AFTER creating the marker).
test_file_conflict_preflight_rejects_before_build() {
t_begin "file_conflict_preflight_rejects_before_build"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkport rt73a <<'EOF'
pkg_name="rt73a"
pkg_ver="1.0"
install:
echo a > $MP_PREFIX/rt73-shared
remove:
rm -f $MP_PREFIX/rt73-shared
EOF
mkport rt73b <<'EOF'
pkg_name="rt73b"
pkg_ver="1.0"
install:
touch $MP_PREFIX/rt73b-build-ran
echo b > $MP_PREFIX/rt73-shared
remove:
rm -f $MP_PREFIX/rt73b-build-ran $MP_PREFIX/rt73-shared
EOF
cat >> "$CONF" <<'EOF'
rt73a:
CONFLICT_PRIORITY=100
rt73b:
CONFLICT_PRIORITY=10
EOF
mp install rt73a >/tmp/rt73.out 2>&1
# rt73b's own manifest seeded DIRECTLY (not through a real install)
# to simulate its own real, if unusual, precondition: a package
# not currently in $db at all, whose manifest nonetheless still
# lists a path from some earlier circumstance (this session's own
# real gcc/cmake db-tracking-gap incidents -- files+manifest
# survive, only the db entry itself goes missing -- are exactly
# this shape). a NORMAL "mp remove" always deletes the manifest
# FILE itself as its own last step, so there's no way to reach
# this state through ordinary commands alone.
mkdir -p "$WD/manifest"
echo "rt73-shared" > "$WD/manifest/rt73b"
out=$(mp install rt73b 2>&1)
rc=$?
# the pre-flight optimization this test originally covered no
# longer exists (removed along with the CONFLICT_PRIORITY bug it
# was built around, see resolve.pm's own comment): every
# CONFLICT_PRIORITY outcome now lets the install proceed to a real
# build, so there's nothing left for a pre-flight check to usefully
# reject early. this now just confirms the real, authoritative
# check still gets the right answer even starting from this same
# unusual "stale manifest, no db entry" precondition.
assert_exit0 "$rc" "$CUR_TEST (install still succeeds overall)" || return
assert_file_exists "$MP_PREFIX/rt73b-build-ran" "$CUR_TEST (install: phase genuinely ran)" || return
assert_contains "$(cat "$MP_PREFIX/rt73-shared")" "a" "$CUR_TEST (winner's own real content restored)" || return
t_pass
}
# fstree.c's own symlink tracking compares by TARGET STRING, not
# mtime (avoids a different, already-fixed false positive) -- meaning
# a genuinely new symlink whose target string happens to already match
# something pre-existing (seeded here, outside mp entirely, before the
# install ever runs) never registers as "changed" on its own. verifies
# expand_symlink_closure catches it anyway, through a two-hop chain
# (real file -> symlink A -> symlink B, BOTH hops pre-seeded with
# their eventual correct value) -- both symlinks must show up in the
# manifest despite neither one's own value ever differing between the
# "before" and "after" snapshot.
test_symlink_closure_catches_stale_value_chain() {
t_begin "symlink_closure_catches_stale_value_chain"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkdir -p "$MP_PREFIX/rt74dir"
ln -sf real-v1 "$MP_PREFIX/rt74dir/real"
ln -sf rt74dir/real "$MP_PREFIX/rt74-cmd"
mkport rt74 <<'EOF'
pkg_name="rt74"
pkg_ver="1.0"
install:
mkdir -p $MP_PREFIX/rt74dir
echo v1 > $MP_PREFIX/rt74dir/real-v1
ln -sf real-v1 $MP_PREFIX/rt74dir/real
ln -sf rt74dir/real $MP_PREFIX/rt74-cmd
remove:
rm -rf $MP_PREFIX/rt74dir $MP_PREFIX/rt74-cmd
EOF
mp install rt74 >/tmp/rt74.out 2>&1
rc=$?
assert_exit0 "$rc" "$CUR_TEST" || return
out=$(mp files rt74 2>&1)
assert_contains "$out" "rt74dir/real-v1" "$CUR_TEST (the genuinely-new real file)" || return
assert_contains "$out" "rt74dir/real" "$CUR_TEST (first-hop symlink, pre-seeded stale value)" || return
assert_contains "$out" "rt74-cmd" "$CUR_TEST (second-hop symlink, pre-seeded stale value)" || return
t_pass
}
# directories are now tracked too (a stable "dir" marker, same
# not-mtime-based reasoning as symlinks -- a shared parent's mtime
# bumps every time ANYTHING inside it changes, so mtime would falsely
# flag it as "changed" for every unrelated package that also drops a
# file there). an EMPTY directory an install creates -- nothing else
# ever implicitly captures it, unlike a real file -- must appear in
# the manifest and actually be removed on "mp remove", not silently
# outlive the package that created it forever.
test_directory_tracked_and_removed() {
t_begin "directory_tracked_and_removed"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkport rt75 <<'EOF'
pkg_name="rt75"
pkg_ver="1.0"
install:
mkdir -p $MP_PREFIX/rt75dir/empty-subdir
echo x > $MP_PREFIX/rt75dir/file.txt
remove:
rm -f $MP_PREFIX/rt75dir/file.txt
EOF
mp install rt75 >/tmp/rt75.out 2>&1
out=$(mp files rt75 2>&1)
assert_contains "$out" "rt75dir/empty-subdir" "$CUR_TEST (empty dir tracked)" || return
assert_file_exists "$MP_PREFIX/rt75dir/empty-subdir" "$CUR_TEST (still on disk before removal)" || return
mp remove rt75 >/tmp/rt75.out 2>&1
assert_file_absent "$MP_PREFIX/rt75dir" "$CUR_TEST (whole dir tree gone after remove)" || return
t_pass
}
# a directory shared between two UNRELATED packages (rt76a creates it
# first, rt76b just drops a file into the already-existing directory
# afterward -- so only rt76a's own manifest ever claims the directory
# itself) must survive removing rt76a while rt76b's own file is still
# there: the automatic manifest sweep's own rmdir has to fail silently
# (ENOTEMPTY) rather than destroying a directory another package is
# still actively using.
test_directory_shared_survives_other_owner() {
t_begin "directory_shared_survives_other_owner"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkport rt76a <<'EOF'
pkg_name="rt76a"
pkg_ver="1.0"
install:
mkdir -p $MP_PREFIX/rt76shared
echo a > $MP_PREFIX/rt76shared/from-a.txt
remove:
rm -f $MP_PREFIX/rt76shared/from-a.txt
EOF
mkport rt76b <<'EOF'
pkg_name="rt76b"
pkg_ver="1.0"
install:
mkdir -p $MP_PREFIX/rt76shared
echo b > $MP_PREFIX/rt76shared/from-b.txt
remove:
rm -f $MP_PREFIX/rt76shared/from-b.txt
EOF
mp install rt76a >/tmp/rt76.out 2>&1
mp install rt76b >>/tmp/rt76.out 2>&1
mp remove rt76a >>/tmp/rt76.out 2>&1
assert_file_exists "$MP_PREFIX/rt76shared/from-b.txt" "$CUR_TEST (rt76b's own file survives)" || return
assert_file_exists "$MP_PREFIX/rt76shared" "$CUR_TEST (shared dir survives, not empty)" || return
t_pass
}
# canon_lock: two REAL, concurrent processes reinstalling the exact
# same already-installed package must not crash racing on its own
# stage directory (confirmed directly, before this lock existed: both
# processes failing outright, "can't open 'remove.sh'"/"can't open
# 'install.sh'"), and must not misattribute a file EITHER one's own
# install: phase touches to the wrong process's own snapshot window.
# rt80's own install: writes its own $$ to a shared file, sleeps (so a
# real overlap window exists if nothing serializes the two), then
# checks the file still holds ITS OWN pid -- a mismatch (only possible
# if the OTHER process's own install: ran WHILE this one's own sleep
# was still in progress) gets logged. no log line existing afterward
# is the actual proof two full reinstall cycles never overlapped.
test_concurrent_reinstall_same_canon_serialized() {
t_begin "concurrent_reinstall_same_canon_serialized"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkport rt80 <<'EOF'
pkg_name="rt80"
pkg_ver="1.0"
install:
echo $$ > $MP_PREFIX/rt80-lockcheck
sleep 1
seen=$(cat $MP_PREFIX/rt80-lockcheck)
if [ "$seen" != "$$" ]; then
echo "$$ saw $seen" >> $MP_PREFIX/rt80-race-log
fi
remove:
rm -f $MP_PREFIX/rt80-lockcheck
EOF
mp install rt80 >/tmp/rt80.out 2>&1
( mp reinstall --yes rt80 >/tmp/rt80a.out 2>&1 ) &
pid1=$!
( mp reinstall --yes rt80 >/tmp/rt80b.out 2>&1 ) &
pid2=$!
wait "$pid1"; rc1=$?
wait "$pid2"; rc2=$?
assert_exit0 "$rc1" "$CUR_TEST (first concurrent reinstall)" || return
assert_exit0 "$rc2" "$CUR_TEST (second concurrent reinstall)" || return
assert_file_absent "$MP_PREFIX/rt80-race-log" "$CUR_TEST (no overlapping install: phase observed)" || return
out=$(mp info rt80 2>&1)
assert_contains "$out" "status: installed" "$CUR_TEST (still tracked installed afterward)" || return
t_pass
}
# USE-conditional dependency atoms (mplib::version::parse_dep_token's
# own "[flag,-flag]" bracket parse; mplib::resolve::apply_use_requirement).
# a dependency atom requiring a flag forces it BEFORE the target's own
# first build -- checked here by having the target's own install phase
# write a SEPARATE marker file only when that flag actually reaches it
# as $USE_<flag>, proving the requirement took effect at build time,
# not just that the command exited 0.
test_use_atom_forces_flag_before_build() {
t_begin "use_atom_forces_flag_before_build"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkport rt50lib <<'EOF'
pkg_name="rt50lib"
pkg_ver="1.0"
pkg_use="feat"
install:
touch $MP_PREFIX/rt50lib-installed
[ -n "$USE_feat" ] && touch $MP_PREFIX/rt50lib-feat-on
remove:
rm -f $MP_PREFIX/rt50lib-installed $MP_PREFIX/rt50lib-feat-on
EOF
mkport rt50consumer <<'EOF'
pkg_name="rt50consumer"
pkg_ver="1.0"
pkg_deps="rt50lib[feat]"
install:
touch $MP_PREFIX/rt50consumer-installed
remove:
rm -f $MP_PREFIX/rt50consumer-installed
EOF
out=$(mp install rt50consumer 2>&1)
rc=$?
assert_exit0 "$rc" "$CUR_TEST" || return
assert_file_exists "$MP_PREFIX/rt50consumer-installed" "$CUR_TEST" || return
assert_file_exists "$MP_PREFIX/rt50lib-feat-on" "$CUR_TEST (USE requirement reached the build)" || return
t_pass
}
# two dependency atoms in the SAME pkg_deps=, requiring CONTRADICTORY
# states on one shared, not-yet-installed target -- must fail rather
# than silently picking one and proceeding. mp resolves pkg_deps=
# sequentially, one token at a time, so by the time the SECOND atom is
# reached the first has already fully built and installed the target
# (with its own requirement in effect) -- meaning this actually
# surfaces via the SAME "already installed, wrong state" path
# test_use_atom_already_installed_wrong_state_fails exercises directly,
# not mplib::resolve::apply_use_requirement's own %USE_REQUIRED_THIS_RUN
# tracking (that guards a narrower window: a requirement recorded by an
# attempt that itself never reached $db, e.g. a soft dependency that
# failed and was skipped, followed by a later, different, non-soft
# consumer wanting the opposite state on the same still-not-installed
# target -- verified directly, at the unit level, not via a full mp
# install scenario here). either way, the actual, END-TO-END guarantee
# this test exists to prove -- two contradictory requirements can never
# both silently take effect -- holds regardless of which internal path
# catches it, which is what's actually being asserted below.
test_use_atom_sequential_conflicting_requirements_fails() {
t_begin "use_atom_sequential_conflicting_requirements_fails"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkport rt51lib <<'EOF'
pkg_name="rt51lib"
pkg_ver="1.0"
pkg_use="feat"
install:
touch $MP_PREFIX/rt51lib-installed
remove:
rm -f $MP_PREFIX/rt51lib-installed
EOF
mkport rt51meta <<'EOF'
pkg_name="rt51meta"
pkg_ver="1.0"
pkg_deps="rt51lib[feat] rt51lib[-feat]"
install:
touch $MP_PREFIX/rt51meta-installed
remove:
rm -f $MP_PREFIX/rt51meta-installed
EOF
out=$(mp install rt51meta 2>&1)
rc=$?
assert_exit_nonzero "$rc" "$CUR_TEST" || return
assert_contains "$out" "without satisfying USE state" "$CUR_TEST" || return
assert_file_absent "$MP_PREFIX/rt51meta-installed" "$CUR_TEST" || return
t_pass
}
# the dependency is ALREADY installed, with a USE state that does NOT
# satisfy a new atom's own requirement -- unlike the not-yet-installed
# case, there is no way to retroactively change what an existing binary
# was actually built with, so this must be a clear, hard failure naming
# what to do (reinstall with the right flags), never a silent
# coexistence or an attempted in-place flip.
test_use_atom_already_installed_wrong_state_fails() {
t_begin "use_atom_already_installed_wrong_state_fails"
reset_state
printf 'GLOBAL_DEPS=" "\n' >> "$CONF"
mkport rt52lib <<'EOF'
pkg_name="rt52lib"
pkg_ver="1.0"
pkg_use="feat"
install:
touch $MP_PREFIX/rt52lib-installed
remove:
rm -f $MP_PREFIX/rt52lib-installed
EOF
mp install rt52lib >/tmp/rt52.out 2>&1
mkport rt52consumer <<'EOF'
pkg_name="rt52consumer"
pkg_ver="1.0"
pkg_deps="rt52lib[feat]"
install:
touch $MP_PREFIX/rt52consumer-installed
remove:
rm -f $MP_PREFIX/rt52consumer-installed
EOF
out=$(mp install rt52consumer 2>&1)
rc=$?
assert_exit_nonzero "$rc" "$CUR_TEST" || return
assert_contains "$out" "without satisfying USE state" "$CUR_TEST" || return
assert_file_absent "$MP_PREFIX/rt52consumer-installed" "$CUR_TEST" || return
t_pass
}
run_resolution_suite() {
test_use_atom_forces_flag_before_build
test_use_atom_sequential_conflicting_requirements_fails
test_use_atom_already_installed_wrong_state_fails
test_slot_conflict_backtrack
test_barename_pin_applies_to_composed_slot
test_blockers_and_mask
test_version_pin
test_providers_target_pin
test_version_stage_ordering
test_masked_provider_skip
test_soft_conflict_warns
test_soft_conflict_auto_removes_unneeded
test_soft_conflict_leaves_transitively_needed_alone
test_subslot_dep_recorded_on_install
test_subslot_change_triggers_dependent_rebuild
test_provider_exhausted_fails
test_fanout_lowest_pref
test_tag_key_pin
test_target_beats_tag_key
test_perpkg_pin_beats_global
test_installed_provider_shortcut
test_soft_conflict_provider_still_picked
test_double_bang_blocker
test_provider_vs_provider_conflict
test_mask_provider_explicit_and_pick
test_deep_fanout_single_pick
test_pin_to_missing_provider_fails
test_conflicts_override
test_file_conflict_no_priority_fails
test_file_conflict_priority_auto_resolves
test_file_conflict_priority_loser_excluded
test_file_conflict_preflight_rejects_before_build
test_symlink_closure_catches_stale_value_chain
test_directory_tracked_and_removed
test_directory_shared_survives_other_owner
test_concurrent_reinstall_same_canon_serialized
}