mirror of
https://github.com/MobAI-App/simslim.git
synced 2026-10-04 21:56:47 +02:00
The repo root becomes `package simslim`, an importable library holding every piece of slimming logic; the CLI moves to `cmd/simslim`. Previously everything was `package main`, so nothing here could be imported at all. The library carries no external dependencies — urfave/cli stays on the CLI side, along with all terminal-facing code: the interactive profile wizard, writeJSON, truncate/humanBytes, fatal and usage. The library never writes to stdout; it returns values and reports progress through the Reporter callback. Renames forced by collisions with existing types: status() -> ReadStatus, statusForDevice() -> ReadStatusForDevice, diskCleanupPlan() -> PlanDiskCleanup. The shutdown/boot timeouts move to simctl.go as ShutdownTimeout/BootTimeout. `go install` gains a path element: github.com/mobai-app/simslim/cmd/simslim. The installed binary is still named simslim, and -X main.version still applies. packaging/homebrew/simslim.rb.in is updated to match, since std_go_args passes no package path. Verified live against a real simulator: list, profiles, status, doctor, size, disk-plan, measure, boot, shutdown, on, off, on --except, on --profile, clone, rename, delete and the interactive wizard all behave identically to the previous build. JSON output, human output, help text and exit codes are unchanged.
214 lines
8.3 KiB
Go
214 lines
8.3 KiB
Go
package simslim
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestDiskCleanupCategoriesAreUniqueAndSafeByDefault(t *testing.T) {
|
|
seen := map[string]bool{}
|
|
for _, category := range DiskCleanupCategories {
|
|
if category.ID == "" || seen[category.ID] {
|
|
t.Fatalf("invalid or duplicate disk cleanup category %q", category.ID)
|
|
}
|
|
seen[category.ID] = true
|
|
if category.DefaultSelected && !category.CanClean {
|
|
t.Fatalf("measured-only category %q must not be selected by default", category.ID)
|
|
}
|
|
}
|
|
if category, ok := diskCleanupCategoryByID("required-siri-assets"); !ok || category.CanClean {
|
|
t.Fatal("required Siri assets must remain measured-only")
|
|
}
|
|
if category, ok := diskCleanupCategoryByID("linguistic-data"); !ok || !category.CanClean || category.DefaultSelected {
|
|
t.Fatal("downloaded language data must be cleanable but opt-in")
|
|
}
|
|
}
|
|
|
|
func TestDiskCleanupRecoveryModelsStayDistinct(t *testing.T) {
|
|
for _, id := range []string{"caches", "logs", "temporary"} {
|
|
category, ok := diskCleanupCategoryByID(id)
|
|
if !ok {
|
|
t.Fatalf("missing cleanup category %q", id)
|
|
}
|
|
if !strings.Contains(category.Recovery, "deleted") {
|
|
t.Errorf("cleanup category %q must say old contents stay deleted: %q", id, category.Recovery)
|
|
}
|
|
}
|
|
|
|
assets, ok := diskCleanupCategoryByID("required-siri-assets")
|
|
if !ok {
|
|
t.Fatal("missing required Siri asset category")
|
|
}
|
|
if !strings.Contains(assets.Recovery, "return automatically") {
|
|
t.Errorf("system assets must describe automatic restoration: %q", assets.Recovery)
|
|
}
|
|
language, ok := diskCleanupCategoryByID("linguistic-data")
|
|
if !ok || !strings.Contains(language.Recovery, "downloads") {
|
|
t.Errorf("language data must describe on-demand recovery: %q", language.Recovery)
|
|
}
|
|
if language.Risk != "Restored on demand" {
|
|
t.Errorf("language data must label its restoration behavior: %q", language.Risk)
|
|
}
|
|
}
|
|
|
|
func TestDiskCleanupPlanAndCleanStayInsideAllowlist(t *testing.T) {
|
|
dataDirectory := filepath.Join(t.TempDir(), "data")
|
|
cacheDirectory := filepath.Join(dataDirectory, "Library", "Caches")
|
|
tempDirectory := filepath.Join(dataDirectory, "Containers", "Data", "Application", "A", "tmp")
|
|
diagnosticsDirectory := filepath.Join(dataDirectory, "var", "db", "diagnostics")
|
|
uuidTextDirectory := filepath.Join(dataDirectory, "var", "db", "uuidtext")
|
|
requiredSiriDirectory := filepath.Join(dataDirectory, "private", "var", "MobileAsset", "AssetsV2", "com_apple_MobileAsset_UAF_Siri_Understanding")
|
|
linguisticDirectory := filepath.Join(dataDirectory, "private", "var", "MobileAsset", "AssetsV2", "com_apple_MobileAsset_LinguisticData")
|
|
unrelatedAssetDirectory := filepath.Join(dataDirectory, "private", "var", "MobileAsset", "AssetsV2", "com_apple_MobileAsset_PKITrustStore")
|
|
appDocumentDirectory := filepath.Join(dataDirectory, "Containers", "Data", "Application", "A", "Documents")
|
|
userNamedCache := filepath.Join(appDocumentDirectory, "Caches", "keep-cache.bin")
|
|
userNamedLogs := filepath.Join(appDocumentDirectory, "Logs", "keep-log.txt")
|
|
userNamedTemp := filepath.Join(appDocumentDirectory, "tmp", "keep-temp.bin")
|
|
assetNamedCache := filepath.Join(requiredSiriDirectory, "Caches", "keep-asset-cache.bin")
|
|
|
|
writeTestFile(t, filepath.Join(cacheDirectory, "cache.bin"), 32*1024)
|
|
writeTestFile(t, filepath.Join(tempDirectory, "temp.bin"), 16*1024)
|
|
writeTestFile(t, filepath.Join(diagnosticsDirectory, "log.tracev3"), 24*1024)
|
|
writeTestFile(t, filepath.Join(uuidTextDirectory, "symbols"), 24*1024)
|
|
writeTestFile(t, filepath.Join(requiredSiriDirectory, "model.bin"), 64*1024)
|
|
writeTestFile(t, filepath.Join(linguisticDirectory, "language-model.bin"), 48*1024)
|
|
writeTestFile(t, filepath.Join(unrelatedAssetDirectory, "trust.bin"), 64*1024)
|
|
writeTestFile(t, filepath.Join(appDocumentDirectory, "user.db"), 64*1024)
|
|
writeTestFile(t, userNamedCache, 32*1024)
|
|
writeTestFile(t, userNamedLogs, 32*1024)
|
|
writeTestFile(t, userNamedTemp, 32*1024)
|
|
writeTestFile(t, assetNamedCache, 32*1024)
|
|
|
|
plan, err := diskCleanupPlanAt("TEST-UDID", dataDirectory)
|
|
if err != nil {
|
|
t.Fatalf("diskCleanupPlanAt() error = %v", err)
|
|
}
|
|
measurements := map[string]DiskCleanupCategoryMeasurement{}
|
|
for _, measurement := range plan.Categories {
|
|
measurements[measurement.ID] = measurement
|
|
}
|
|
for _, id := range []string{"caches", "logs", "temporary", "linguistic-data", "required-siri-assets"} {
|
|
if measurements[id].Bytes == 0 {
|
|
t.Errorf("category %q should report test data", id)
|
|
}
|
|
}
|
|
if plan.CleanableBytes == 0 || plan.TotalBytes <= plan.CleanableBytes {
|
|
t.Errorf("unexpected plan totals: cleanable=%d total=%d", plan.CleanableBytes, plan.TotalBytes)
|
|
}
|
|
|
|
result, err := cleanDiskAt("TEST-UDID", dataDirectory, []string{"temporary", "logs", "caches", "linguistic-data"})
|
|
if err != nil {
|
|
t.Fatalf("cleanDiskAt() error = %v", err)
|
|
}
|
|
if result.ReclaimedBytes == 0 {
|
|
t.Error("cleanup should reclaim allocated bytes")
|
|
}
|
|
for _, removed := range []string{
|
|
filepath.Join(cacheDirectory, "cache.bin"),
|
|
filepath.Join(tempDirectory, "temp.bin"),
|
|
filepath.Join(diagnosticsDirectory, "log.tracev3"),
|
|
filepath.Join(uuidTextDirectory, "symbols"),
|
|
filepath.Join(linguisticDirectory, "language-model.bin"),
|
|
} {
|
|
if _, err := os.Lstat(removed); !os.IsNotExist(err) {
|
|
t.Errorf("cleanup left %s", removed)
|
|
}
|
|
}
|
|
for _, preservedDirectory := range []string{
|
|
cacheDirectory,
|
|
tempDirectory,
|
|
diagnosticsDirectory,
|
|
uuidTextDirectory,
|
|
linguisticDirectory,
|
|
} {
|
|
if info, err := os.Lstat(preservedDirectory); err != nil || !info.IsDir() {
|
|
t.Errorf("cleanup must preserve directory %s", preservedDirectory)
|
|
}
|
|
}
|
|
for _, preserved := range []string{
|
|
filepath.Join(requiredSiriDirectory, "model.bin"),
|
|
filepath.Join(unrelatedAssetDirectory, "trust.bin"),
|
|
filepath.Join(appDocumentDirectory, "user.db"),
|
|
userNamedCache,
|
|
userNamedLogs,
|
|
userNamedTemp,
|
|
assetNamedCache,
|
|
} {
|
|
if _, err := os.Lstat(preserved); err != nil {
|
|
t.Errorf("cleanup touched non-cleanable data %s: %v", preserved, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDiskCleanupRejectsIntermediateSymlink(t *testing.T) {
|
|
root := t.TempDir()
|
|
dataDirectory := filepath.Join(root, "data")
|
|
outsideLogDirectory := filepath.Join(root, "outside", "log")
|
|
outsideFile := filepath.Join(outsideLogDirectory, "keep.log")
|
|
writeTestFile(t, outsideFile, 4096)
|
|
if err := os.MkdirAll(dataDirectory, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(filepath.Dir(outsideLogDirectory), filepath.Join(dataDirectory, "var")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := cleanDiskAt("TEST-UDID", dataDirectory, []string{"logs"}); err == nil {
|
|
t.Fatal("cleanup should reject a target reached through an intermediate symlink")
|
|
}
|
|
if _, err := os.Lstat(outsideFile); err != nil {
|
|
t.Fatalf("cleanup touched data outside the simulator root: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDiskCleanupDoesNotFollowSymlinks(t *testing.T) {
|
|
root := t.TempDir()
|
|
dataDirectory := filepath.Join(root, "data")
|
|
cacheDirectory := filepath.Join(dataDirectory, "Library", "Caches")
|
|
outsideDirectory := filepath.Join(root, "outside")
|
|
outsideFile := filepath.Join(outsideDirectory, "keep.txt")
|
|
writeTestFile(t, outsideFile, 4096)
|
|
if err := os.MkdirAll(cacheDirectory, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(outsideDirectory, filepath.Join(cacheDirectory, "outside-link")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := cleanDiskAt("TEST-UDID", dataDirectory, []string{"caches"}); err != nil {
|
|
t.Fatalf("cleanDiskAt() error = %v", err)
|
|
}
|
|
if _, err := os.Lstat(outsideFile); err != nil {
|
|
t.Fatalf("cleanup followed a symlink outside simulator data: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDiskCleanupRejectsUnsafeTargetsAndMeasuredOnlyCategory(t *testing.T) {
|
|
root := t.TempDir()
|
|
dataDirectory := filepath.Join(root, "data")
|
|
if err := os.MkdirAll(dataDirectory, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := requireDescendant(dataDirectory, dataDirectory); err == nil {
|
|
t.Error("requireDescendant should reject the data root itself")
|
|
}
|
|
if err := requireDescendant(dataDirectory, root); err == nil {
|
|
t.Error("requireDescendant should reject a parent directory")
|
|
}
|
|
if _, err := ValidateDiskCleanupSelection([]string{"required-siri-assets"}); err == nil {
|
|
t.Error("measured-only MobileAsset cleanup must be rejected")
|
|
}
|
|
}
|
|
|
|
func writeTestFile(t *testing.T, target string, size int) {
|
|
t.Helper()
|
|
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(target, make([]byte, size), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|