Skip to content

Commit db4ff14

Browse files
committed
more
1 parent 8ce6bb5 commit db4ff14

15 files changed

Lines changed: 1013 additions & 43 deletions

‎MODULE.bazel‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ go_sdk.download(version = GO_VERSION)
9999

100100
go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
101101
go_deps.from_file(go_mod = "//:go.mod")
102-
use_repo(go_deps, "com_github_google_go_cmp", "org_golang_x_sys")
102+
use_repo(go_deps, "com_github_google_go_cmp", "org_golang_x_sys", "com_github_stretchr_testify")
103103

104104
bazel_dep(name = "rules_java", version = "9.6.1")
105105

‎go.mod‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,8 @@ go 1.25.0
55
require github.com/google/go-cmp v0.6.0
66

77
require golang.org/x/sys v0.47.0
8+
9+
require (
10+
github.com/stretchr/testify v1.12.1
11+
go.yaml.in/yaml/v3 v3.0.5 // indirect
12+
)

‎go.sum‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
22
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
3+
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
4+
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
5+
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
6+
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
37
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
48
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=

‎tools/simulator_manager/go/BUILD.bazel‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
load("@rules_go//go:def.bzl", "go_binary", "go_library")
1+
load("@rules_go//go:def.bzl", "go_binary", "go_library", "go_test")
22

33
go_library(
44
name = "go_lib",
@@ -23,3 +23,22 @@ go_binary(
2323
embed = [":go_lib"],
2424
visibility = ["//visibility:public"],
2525
)
26+
27+
go_test(
28+
name = "go_test",
29+
srcs = [
30+
"fake_simulator_control_test.go",
31+
"http_server_test.go",
32+
"lease_store_test.go",
33+
"lru_set_test.go",
34+
"main_test.go",
35+
"simulator_control_test.go",
36+
"simulator_manager_test.go",
37+
"simulator_request_handler_test.go",
38+
],
39+
embed = [":go_lib"],
40+
deps = [
41+
"@com_github_stretchr_testify//assert",
42+
"@com_github_stretchr_testify//require",
43+
],
44+
)
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"sync"
6+
)
7+
8+
// fakeSimulatorControl is an in-memory SimulatorControl used to test
9+
// SimulatorManager's lease/slot/reference-count logic without shelling out to
10+
// xcrun simctl.
11+
type fakeSimulatorControl struct {
12+
mu sync.Mutex
13+
14+
udidCounter int
15+
16+
createBaseCalls int
17+
18+
cloneCalls []string // clone device names, in call order
19+
// cloneGate, if non-nil, blocks the first Clone call until the test sends
20+
// (or closes) it -- used to deterministically exercise in-flight clone
21+
// sharing between concurrent Lease calls.
22+
cloneGate chan struct{}
23+
cloneGateOnce sync.Once
24+
cloneGateFired bool
25+
26+
ensureBootedCalls []SimulatorUDID
27+
// ensureBootedErrs are one-shot: each error is returned exactly once for
28+
// the matching UDID, then cleared, so a retry can succeed.
29+
ensureBootedErrs map[SimulatorUDID]error
30+
31+
deleteCalls []SimulatorUDID
32+
deleteErrs map[SimulatorUDID]error
33+
34+
cleanTempFilesCalls []SimulatorUDID
35+
}
36+
37+
func newFakeSimulatorControl() *fakeSimulatorControl {
38+
return &fakeSimulatorControl{
39+
ensureBootedErrs: make(map[SimulatorUDID]error),
40+
deleteErrs: make(map[SimulatorUDID]error),
41+
}
42+
}
43+
44+
func (f *fakeSimulatorControl) nextUDID() SimulatorUDID {
45+
f.udidCounter++
46+
return fmt.Sprintf("udid-%d", f.udidCounter)
47+
}
48+
49+
func (f *fakeSimulatorControl) CreateBase(name string, config SimulatorConfig, runtimeIdentifier string) (SimulatorUDID, error) {
50+
f.mu.Lock()
51+
f.createBaseCalls++
52+
udid := f.nextUDID()
53+
f.mu.Unlock()
54+
return udid, nil
55+
}
56+
57+
func (f *fakeSimulatorControl) Clone(baseSimulator SimulatorUDID, name string, deviceType string, runtimeIdentifier string, postBoot *string) (SimulatorUDID, error) {
58+
f.mu.Lock()
59+
f.cloneCalls = append(f.cloneCalls, name)
60+
gate := f.cloneGate
61+
alreadyFired := f.cloneGateFired
62+
f.mu.Unlock()
63+
64+
if gate != nil && !alreadyFired {
65+
f.cloneGateOnce.Do(func() {
66+
<-gate
67+
f.mu.Lock()
68+
f.cloneGateFired = true
69+
f.mu.Unlock()
70+
})
71+
}
72+
73+
f.mu.Lock()
74+
udid := f.nextUDID()
75+
f.mu.Unlock()
76+
return udid, nil
77+
}
78+
79+
func (f *fakeSimulatorControl) EnsureBooted(simulator SimulatorUDID, context string) error {
80+
f.mu.Lock()
81+
defer f.mu.Unlock()
82+
83+
f.ensureBootedCalls = append(f.ensureBootedCalls, simulator)
84+
if err, ok := f.ensureBootedErrs[simulator]; ok {
85+
delete(f.ensureBootedErrs, simulator)
86+
return err
87+
}
88+
return nil
89+
}
90+
91+
func (f *fakeSimulatorControl) CleanTempFiles(simulator SimulatorUDID) {
92+
f.mu.Lock()
93+
defer f.mu.Unlock()
94+
f.cleanTempFilesCalls = append(f.cleanTempFilesCalls, simulator)
95+
}
96+
97+
func (f *fakeSimulatorControl) Delete(simulator SimulatorUDID, name string, context string) error {
98+
f.mu.Lock()
99+
defer f.mu.Unlock()
100+
101+
f.deleteCalls = append(f.deleteCalls, simulator)
102+
return f.deleteErrs[simulator]
103+
}
104+
105+
func (f *fakeSimulatorControl) GetExisting(name string, deviceType string, runtimeIdentifier string, context string) (string, error) {
106+
// SimulatorManager never calls this directly -- it's only used internally
107+
// by RealSimulatorControl's own createBase/clone rediscovery logic, which
108+
// this fake doesn't need to replicate.
109+
return "", nil
110+
}
111+
112+
// fakeLeaseStore is an in-memory LeaseStore for testing RestoreLeases and
113+
// persistence side effects without touching disk.
114+
type fakeLeaseStore struct {
115+
mu sync.Mutex
116+
loaded []PersistedLease
117+
saved [][]PersistedLease
118+
}
119+
120+
func (f *fakeLeaseStore) Load() []PersistedLease {
121+
f.mu.Lock()
122+
defer f.mu.Unlock()
123+
return f.loaded
124+
}
125+
126+
func (f *fakeLeaseStore) Save(leases []PersistedLease) {
127+
f.mu.Lock()
128+
defer f.mu.Unlock()
129+
f.saved = append(f.saved, leases)
130+
}
131+
132+
func (f *fakeLeaseStore) lastSaved() []PersistedLease {
133+
f.mu.Lock()
134+
defer f.mu.Unlock()
135+
if len(f.saved) == 0 {
136+
return nil
137+
}
138+
return f.saved[len(f.saved)-1]
139+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package main
2+
3+
// These tests exercise HTTPServer's HTTP handler methods directly via
4+
// net/http/httptest, bypassing Run(). Run() itself (unix socket setup, PID
5+
// file management, OS signal handling) and handleShutdownRequest (which
6+
// calls os.Exit) are integration-level, OS-facing plumbing that would
7+
// require killing or replacing the test process itself to exercise safely,
8+
// so they're intentionally left uncovered here.
9+
10+
import (
11+
"net/http"
12+
"net/http/httptest"
13+
"net/url"
14+
"testing"
15+
16+
"github.com/stretchr/testify/assert"
17+
"github.com/stretchr/testify/require"
18+
)
19+
20+
func TestHandleVersionRequest(t *testing.T) {
21+
control := newFakeSimulatorControl()
22+
sm := newTestManager(control, 0, 0, false, nil)
23+
server := NewHTTPServer(NewSimulatorRequestHandler(sm), "v1.2.3")
24+
25+
req := httptest.NewRequest(http.MethodGet, "/version", nil)
26+
rec := httptest.NewRecorder()
27+
28+
server.handleVersionRequest(rec, req)
29+
30+
assert.Equal(t, http.StatusOK, rec.Code)
31+
assert.Equal(t, "v1.2.3\n", rec.Body.String())
32+
}
33+
34+
func TestHandleLeasesRequest(t *testing.T) {
35+
control := newFakeSimulatorControl()
36+
sm := newTestManager(control, 0, 0, false, nil)
37+
server := NewHTTPServer(NewSimulatorRequestHandler(sm), "v1")
38+
39+
req := httptest.NewRequest(http.MethodGet, "/leases", nil)
40+
rec := httptest.NewRecorder()
41+
42+
server.handleLeasesRequest(rec, req)
43+
44+
assert.Equal(t, http.StatusOK, rec.Code)
45+
assert.Equal(t, "0\n", rec.Body.String())
46+
}
47+
48+
func TestHandleSimulatorRequest_RoutesToHandler(t *testing.T) {
49+
control := newFakeSimulatorControl()
50+
sm := newTestManager(control, 0, 0, false, nil)
51+
server := NewHTTPServer(NewSimulatorRequestHandler(sm), "v1")
52+
53+
params := url.Values{"exclusive": {"1"}, "deviceType": {"iPhone"}, "os": {"iOS"}, "version": {"17.0"}}
54+
req := httptest.NewRequest(http.MethodPost, "/simulator/1?"+params.Encode(), nil)
55+
rec := httptest.NewRecorder()
56+
57+
server.handleSimulatorRequest(rec, req)
58+
59+
require.Equal(t, http.StatusCreated, rec.Code)
60+
assert.NotEmpty(t, rec.Body.String())
61+
}
62+
63+
func TestHandleSimulatorRequest_BadRequestSurfacesMessage(t *testing.T) {
64+
control := newFakeSimulatorControl()
65+
sm := newTestManager(control, 0, 0, false, nil)
66+
server := NewHTTPServer(NewSimulatorRequestHandler(sm), "v1")
67+
68+
req := httptest.NewRequest(http.MethodDelete, "/simulator/1", nil)
69+
rec := httptest.NewRecorder()
70+
71+
server.handleSimulatorRequest(rec, req)
72+
73+
assert.Equal(t, http.StatusNotFound, rec.Code)
74+
assert.Contains(t, rec.Body.String(), "doesn't have a simulator leased")
75+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestFileLeaseStore_LoadNonexistentFile(t *testing.T) {
13+
store := NewFileLeaseStore(filepath.Join(t.TempDir(), "does-not-exist.json"))
14+
assert.Nil(t, store.Load())
15+
}
16+
17+
func TestFileLeaseStore_SaveAndLoadRoundTrip(t *testing.T) {
18+
path := filepath.Join(t.TempDir(), "leases.json")
19+
store := NewFileLeaseStore(path)
20+
21+
startTime := uint64(123)
22+
leases := []PersistedLease{
23+
{
24+
PID: 42,
25+
LeaserStartTime: &startTime,
26+
UDID: "udid-1",
27+
Config: SimulatorConfig{DeviceType: "iPhone", OS: "iOS", Version: "17.0"},
28+
Exclusive: true,
29+
SlotIndex: 0,
30+
},
31+
{
32+
PID: 43,
33+
UDID: "udid-2",
34+
Config: SimulatorConfig{DeviceType: "iPad", OS: "iOS", Version: "17.0"},
35+
Exclusive: false,
36+
SlotIndex: 1,
37+
},
38+
}
39+
40+
store.Save(leases)
41+
42+
loaded := store.Load()
43+
require.Equal(t, leases, loaded)
44+
}
45+
46+
func TestFileLeaseStore_SaveIsAtomic(t *testing.T) {
47+
path := filepath.Join(t.TempDir(), "leases.json")
48+
store := NewFileLeaseStore(path)
49+
50+
store.Save([]PersistedLease{{PID: 1, UDID: "udid-1"}})
51+
store.Save([]PersistedLease{{PID: 2, UDID: "udid-2"}})
52+
53+
// The temp file used for the atomic rename should never be left behind.
54+
_, err := os.Stat(path + ".tmp")
55+
assert.True(t, os.IsNotExist(err))
56+
57+
loaded := store.Load()
58+
require.Len(t, loaded, 1)
59+
assert.Equal(t, int32(2), loaded[0].PID)
60+
}
61+
62+
func TestFileLeaseStore_LoadCorruptFileReturnsNilInsteadOfCrashing(t *testing.T) {
63+
path := filepath.Join(t.TempDir(), "leases.json")
64+
require.NoError(t, os.WriteFile(path, []byte("not valid json"), 0644))
65+
66+
store := NewFileLeaseStore(path)
67+
assert.Nil(t, store.Load())
68+
}
69+
70+
func TestProcessStartTime_CurrentProcessIsStable(t *testing.T) {
71+
pid := int32(os.Getpid())
72+
73+
first := processStartTime(pid)
74+
require.NotNil(t, first, "should be able to read the current process's own start time")
75+
76+
second := processStartTime(pid)
77+
require.NotNil(t, second)
78+
79+
assert.Equal(t, *first, *second, "the start time of a still-running process must not change between calls")
80+
}
81+
82+
func TestProcessStartTime_ExitedProcessReturnsNil(t *testing.T) {
83+
assert.Nil(t, processStartTime(spawnDeadPID(t)))
84+
}

‎tools/simulator_manager/go/lru_set.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ func NewLRUSet[T comparable](capacity int) *LRUSet[T] {
2525
func (s *LRUSet[T]) Insert(element T) *T {
2626
var evicted *T
2727
if _, exists := s.storage[element]; exists {
28-
// Remove from current position in order
28+
// Remove from current position in order. Nothing is evicted here --
29+
// the element is already in the set, just moving to the
30+
// most-recently-used end.
2931
for i, e := range s.order {
3032
if e == element {
31-
old := s.order[i]
32-
evicted = &old
3333
s.order = append(s.order[:i], s.order[i+1:]...)
3434
break
3535
}

0 commit comments

Comments
 (0)