unsloth/tests/sh/test_uninstall_sd_cpp_custom_root.sh
Daniel Han fb94a79337 Studio: fix dataset upload data loss, caption over-count, and custom-root sd.cpp uninstall
- Diffusion dataset upload now streams each file into a sibling temp file and
  atomically os.replace()s it into place only after the whole file is written and
  within the size cap. A mid-batch 413 (or any abort) removes the temp, never an
  example already stored under the same name, so re-uploading a too-large batch can
  no longer truncate or delete a previously uploaded image.
- _diffusion_dataset_summary counts an image as captioned only when it resolves to a
  non-empty caption via the same sidecar-over-metadata precedence the trainer uses. An
  empty (tombstone) sidecar shadows a metadata row and makes the trainer skip the
  image, so counting it over-reported caption_count and mislabeled an effectively
  uncaptioned dataset as captioned.
- uninstall.sh/.ps1 now remove a custom/env-mode Studio's native diffusion build that
  installs beside the root as a stable-diffusion.cpp sibling (find_sd_cpp_binary
  resolves it from the Studio home's parent), guarded by the same unsafe-path check,
  and stop processes locking the default-mode stable-diffusion.cpp before removing it.

Adds regression tests for the upload data-loss and caption-count paths and a hermetic
shell test for the custom-root stable-diffusion.cpp removal.
2026-07-07 02:16:56 +00:00

85 lines
3.8 KiB
Bash
Executable file

#!/bin/bash
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
# Unit test for custom/env-mode stable-diffusion.cpp removal in scripts/uninstall.sh.
#
# A custom Studio (UNSLOTH_STUDIO_HOME=<root>) installs its native diffusion build beside
# the root at <parent>/stable-diffusion.cpp -- find_sd_cpp_binary resolves it from
# UNSLOTH_STUDIO_HOME.parent (sd_cpp_engine.py). Uninstall must remove that sibling too, or
# a stale build lingers and a fresh install's finder can pick it up. Tested hermetically:
# the real custom-root removal loop + its helpers are extracted from uninstall.sh and run
# against per-test fixtures. Follows the extract-via-sed pattern of test_uninstall_shared_icon.sh.
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
UNINSTALL_SH="$SCRIPT_DIR/../../scripts/uninstall.sh"
PASS=0
FAIL=0
_TMP_ROOT=$(mktemp -d)
trap 'rm -rf "$_TMP_ROOT"' EXIT
# Deterministic deny-list checks: keep $HOME clear of the fixture trees.
HOME="$_TMP_ROOT/home"
mkdir -p "$HOME"
assert_nodir() { _l="$1"; [ -d "$2" ] && { echo " FAIL: $_l (still present: $2)"; FAIL=$((FAIL+1)); } || { echo " PASS: $_l"; PASS=$((PASS+1)); }; }
assert_dir() { _l="$1"; [ -d "$2" ] && { echo " PASS: $_l"; PASS=$((PASS+1)); } || { echo " FAIL: $_l (missing dir $2)"; FAIL=$((FAIL+1)); }; }
# Extract the helpers the loop depends on, plus the real custom-root removal loop.
HELPERS_FILE=$(mktemp -p "$_TMP_ROOT")
{
sed -n '/^_remove_path() {/,/^}/p' "$UNINSTALL_SH"
sed -n '/^_is_studio_root() {/,/^}/p' "$UNINSTALL_SH"
sed -n '/^_is_unsafe_root() {/,/^}/p' "$UNINSTALL_SH"
} > "$HELPERS_FILE"
LOOP_FILE=$(mktemp -p "$_TMP_ROOT")
sed -n '/^_custom_studio_roots | while IFS= read -r _custom_root; do/,/^done/p' "$UNINSTALL_SH" > "$LOOP_FILE"
# shellcheck disable=SC1090
. "$HELPERS_FILE"
# make_studio <root> : a valid custom Studio root (share/studio.conf owner marker) plus its
# sibling <parent>/stable-diffusion.cpp build, each with a file so removal is observable.
make_studio() {
mkdir -p "$1/share"
: > "$1/share/studio.conf"
_sib="$(dirname "$1")/stable-diffusion.cpp"
mkdir -p "$_sib"
: > "$_sib/sd-cli"
}
run_loop() {
# shellcheck disable=SC1090
. "$LOOP_FILE"
}
# 1. Single custom root -> root AND its sibling stable-diffusion.cpp both removed.
p1="$_TMP_ROOT/inst1"
make_studio "$p1/studioA"
: > "$p1/keep.txt" # unrelated sibling file must be untouched
_custom_studio_roots() { printf '%s\n' "$p1/studioA"; }
run_loop
assert_nodir "single custom root removed" "$p1/studioA"
assert_nodir "custom-root sibling stable-diffusion.cpp removed" "$p1/stable-diffusion.cpp"
[ -f "$p1/keep.txt" ] && { echo " PASS: unrelated sibling file kept"; PASS=$((PASS+1)); } || { echo " FAIL: unrelated sibling file removed"; FAIL=$((FAIL+1)); }
# 2. Two custom roots sharing a parent share one sd.cpp -> all removed, no error on the
# second (already-gone) removal.
p2="$_TMP_ROOT/inst2"
make_studio "$p2/studioB"
make_studio "$p2/studioC" # same parent -> same sibling sd.cpp
_custom_studio_roots() { printf '%s\n%s\n' "$p2/studioB" "$p2/studioC"; }
run_loop
assert_nodir "shared-parent root B removed" "$p2/studioB"
assert_nodir "shared-parent root C removed" "$p2/studioC"
assert_nodir "shared sibling stable-diffusion.cpp removed" "$p2/stable-diffusion.cpp"
# 3. Default-mode sd.cpp (a bare ~/.unsloth/stable-diffusion.cpp with no custom root) is NOT
# touched by the custom-root loop -- it is removed by the separate default-mode line.
mkdir -p "$HOME/.unsloth/stable-diffusion.cpp"
_custom_studio_roots() { printf '%s\n' "$p1/studioA"; } # a now-removed root -> guard skips
run_loop
assert_dir "default-mode sd.cpp untouched by custom loop" "$HOME/.unsloth/stable-diffusion.cpp"
echo ""
echo "Results: $PASS passed, $FAIL failed"
[ "$FAIL" = 0 ]