Files
simslim-mirror/cmd/simslim/app_test.go
Interlap ed35f10813 feat: make boot timeout configurable for slow CI runners (#15)
`simslim on` boots, disables ~170 daemons one launchctl call at a time,
then reboots, all under a single BootTimeout deadline. On slow shared CI
runners the per-daemon transitions can exhaust that budget mid-reconfigure,
failing with `context deadline exceeded` (issue #13).

Make BootTimeout a package var (default raised 6m -> 10m) overridable via a
global `--boot-timeout` flag and the SIMSLIM_BOOT_TIMEOUT env var. Every
existing context.WithTimeout(ctx, simslim.BootTimeout) call site picks it up.

Closes #13
2026-07-23 13:37:28 +02:00

136 lines
5.4 KiB
Go

package main
import (
"context"
"os"
"reflect"
"testing"
"time"
"github.com/mobai-app/simslim"
)
// runApp runs the CLI with stdout/stderr discarded so command output does not
// pollute the test log. It uses `profiles`, which touches neither simctl nor the
// macOS-only guard, to exercise flag and argument parsing through the real tree.
func runApp(t *testing.T, args ...string) error {
t.Helper()
devnull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
if err != nil {
t.Fatalf("open %s: %v", os.DevNull, err)
}
defer devnull.Close()
oldStdout, oldStderr := os.Stdout, os.Stderr
os.Stdout, os.Stderr = devnull, devnull
defer func() { os.Stdout, os.Stderr = oldStdout, oldStderr }()
return newApp().Run(context.Background(), append([]string{"simslim"}, args...))
}
// TestAppRegistersDeviceSets verifies the global --set flag registers the extra
// device sets, whether it appears before or after the subcommand, in equals
// form, comma-separated, or repeated (last-wins), and that a blank value errors.
func TestAppRegistersDeviceSets(t *testing.T) {
tests := []struct {
name string
args []string
wantExtra []string // tokens registered beyond the well-known sets
wantError bool
}{
{name: "absent", args: []string{"profiles"}},
{name: "before command", args: []string{"--set", "/a", "profiles"}, wantExtra: []string{"/a"}},
{name: "after command", args: []string{"profiles", "--set", "/a"}, wantExtra: []string{"/a"}},
{name: "equals form", args: []string{"profiles", "--set=/a"}, wantExtra: []string{"/a"}},
{name: "comma-separated", args: []string{"--set", "/a,/b", "profiles"}, wantExtra: []string{"/a", "/b"}},
{name: "comma trims blanks", args: []string{"--set=/a, ,/b", "profiles"}, wantExtra: []string{"/a", "/b"}},
{name: "repeat is last-wins", args: []string{"--set", "/a", "--set", "/b", "profiles"}, wantExtra: []string{"/b"}},
{name: "well-known not duplicated", args: []string{"--set", "testing", "profiles"}},
{name: "blank value errors", args: []string{"--set", " ", "profiles"}, wantError: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
simslim.ResetDeviceSets()
defer simslim.ResetDeviceSets()
err := runApp(t, tt.args...)
if (err != nil) != tt.wantError {
t.Fatalf("Run(%v) error = %v, wantError %v", tt.args, err, tt.wantError)
}
gotExtra := simslim.ExtraDeviceSetTokens()
if !reflect.DeepEqual(gotExtra, tt.wantExtra) {
t.Errorf("registered extra sets = %v, want %v", gotExtra, tt.wantExtra)
}
})
}
}
// TestAppBootTimeoutFlag verifies --boot-timeout and its SIMSLIM_BOOT_TIMEOUT env
// source override simslim.BootTimeout, and that a non-positive duration is rejected.
func TestAppBootTimeoutFlag(t *testing.T) {
const def = 10 * time.Minute
tests := []struct {
name string
args []string
env string // value for SIMSLIM_BOOT_TIMEOUT, "" to leave unset
want time.Duration
wantError bool
}{
{name: "absent keeps default", args: []string{"profiles"}, want: def},
{name: "before command", args: []string{"--boot-timeout", "15m", "profiles"}, want: 15 * time.Minute},
{name: "after command", args: []string{"profiles", "--boot-timeout", "15m"}, want: 15 * time.Minute},
{name: "equals form", args: []string{"profiles", "--boot-timeout=20m"}, want: 20 * time.Minute},
{name: "from env", args: []string{"profiles"}, env: "12m", want: 12 * time.Minute},
{name: "flag overrides env", args: []string{"profiles", "--boot-timeout", "9m"}, env: "12m", want: 9 * time.Minute},
{name: "zero rejected", args: []string{"profiles", "--boot-timeout", "0"}, want: def, wantError: true},
{name: "negative rejected", args: []string{"profiles", "--boot-timeout", "-1m"}, want: def, wantError: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
simslim.ResetDeviceSets()
defer simslim.ResetDeviceSets()
orig := simslim.BootTimeout
simslim.BootTimeout = def
defer func() { simslim.BootTimeout = orig }()
if tt.env != "" {
t.Setenv("SIMSLIM_BOOT_TIMEOUT", tt.env)
}
err := runApp(t, tt.args...)
if (err != nil) != tt.wantError {
t.Fatalf("Run(%v) error = %v, wantError %v", tt.args, err, tt.wantError)
}
if simslim.BootTimeout != tt.want {
t.Errorf("BootTimeout = %v, want %v", simslim.BootTimeout, tt.want)
}
})
}
}
// TestAppFlagParsing covers per-command flag parsing: a flag may appear before
// or after a positional argument, --json rejects duplicates, an unknown flag is
// rejected, and an unknown positional fails validation.
func TestAppFlagParsing(t *testing.T) {
tests := []struct {
name string
args []string
wantError bool
}{
{name: "json flag", args: []string{"profiles", "--json"}},
{name: "flag before positional", args: []string{"profiles", "--json", "siri"}},
{name: "flag after positional", args: []string{"profiles", "siri", "--json"}},
{name: "duplicate json rejected", args: []string{"profiles", "--json", "--json", "siri"}, wantError: true},
{name: "unknown flag rejected", args: []string{"profiles", "--nope"}, wantError: true},
{name: "unknown category rejected", args: []string{"profiles", "does-not-exist"}, wantError: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
simslim.ResetDeviceSets()
defer simslim.ResetDeviceSets()
err := runApp(t, tt.args...)
if (err != nil) != tt.wantError {
t.Fatalf("Run(%v) error = %v, wantError %v", tt.args, err, tt.wantError)
}
})
}
}