| package instance_test |
|
|
| import ( |
| "fmt" |
| "testing" |
|
|
| "github.com/pinchtab/pinchtab/internal/allocation" |
| bridgepkg "github.com/pinchtab/pinchtab/internal/bridge" |
| "github.com/pinchtab/pinchtab/internal/instance" |
| ) |
|
|
| |
|
|
| type mockLauncher struct { |
| instances map[string]*bridgepkg.Instance |
| nextID int |
| stopErr error |
| } |
|
|
| func newMockLauncher() *mockLauncher { |
| return &mockLauncher{instances: make(map[string]*bridgepkg.Instance)} |
| } |
|
|
| func (m *mockLauncher) Launch(name, port string, headless bool) (*bridgepkg.Instance, error) { |
| m.nextID++ |
| inst := &bridgepkg.Instance{ |
| ID: fmt.Sprintf("inst_%d", m.nextID), |
| ProfileName: name, |
| Port: port, |
| URL: "http://localhost:" + port, |
| Headless: headless, |
| Status: "running", |
| } |
| m.instances[inst.ID] = inst |
| return inst, nil |
| } |
|
|
| func (m *mockLauncher) Stop(id string) error { |
| if m.stopErr != nil { |
| return m.stopErr |
| } |
| delete(m.instances, id) |
| return nil |
| } |
|
|
| type mockFetcher struct { |
| |
| tabsByURL map[string][]bridgepkg.InstanceTab |
| } |
|
|
| func newMockFetcher() *mockFetcher { |
| return &mockFetcher{tabsByURL: make(map[string][]bridgepkg.InstanceTab)} |
| } |
|
|
| func (f *mockFetcher) FetchTabs(instanceURL string) ([]bridgepkg.InstanceTab, error) { |
| tabs, ok := f.tabsByURL[instanceURL] |
| if !ok { |
| return nil, fmt.Errorf("instance %s not reachable", instanceURL) |
| } |
| return tabs, nil |
| } |
|
|
| func (f *mockFetcher) AddTab(instancePort, tabID, url string) { |
| key := "http://localhost:" + instancePort |
| f.AddTabForURL(key, tabID, url) |
| } |
|
|
| func (f *mockFetcher) AddTabForURL(instanceURL, tabID, url string) { |
| key := instanceURL |
| f.tabsByURL[key] = append(f.tabsByURL[key], bridgepkg.InstanceTab{ |
| ID: tabID, |
| URL: url, |
| }) |
| } |
|
|
| |
|
|
| func TestRepository_LaunchAndGet(t *testing.T) { |
| launcher := newMockLauncher() |
| repo := instance.NewRepository(launcher) |
|
|
| inst, err := repo.Launch("default", "9868", true) |
| if err != nil { |
| t.Fatal(err) |
| } |
|
|
| got, ok := repo.Get(inst.ID) |
| if !ok { |
| t.Fatal("instance not found after launch") |
| } |
| if got.ProfileName != "default" { |
| t.Errorf("expected profile default, got %s", got.ProfileName) |
| } |
| if repo.Count() != 1 { |
| t.Errorf("expected count 1, got %d", repo.Count()) |
| } |
| } |
|
|
| func TestRepository_StopRemovesInstance(t *testing.T) { |
| launcher := newMockLauncher() |
| repo := instance.NewRepository(launcher) |
|
|
| inst, _ := repo.Launch("default", "9868", true) |
| if err := repo.Stop(inst.ID); err != nil { |
| t.Fatal(err) |
| } |
| if _, ok := repo.Get(inst.ID); ok { |
| t.Error("instance should be gone after stop") |
| } |
| if repo.Count() != 0 { |
| t.Errorf("expected count 0, got %d", repo.Count()) |
| } |
| } |
|
|
| func TestRepository_Running_FiltersNonRunning(t *testing.T) { |
| launcher := newMockLauncher() |
| repo := instance.NewRepository(launcher) |
|
|
| inst1, _ := repo.Launch("prof1", "9868", true) |
| _, _ = repo.Launch("prof2", "9869", true) |
|
|
| |
| stopped := *inst1 |
| stopped.Status = "stopped" |
| repo.Add(&stopped) |
|
|
| running := repo.Running() |
| if len(running) != 1 { |
| t.Errorf("expected 1 running, got %d", len(running)) |
| } |
| } |
|
|
| |
|
|
| func TestLocator_CacheHit(t *testing.T) { |
| launcher := newMockLauncher() |
| fetcher := newMockFetcher() |
| repo := instance.NewRepository(launcher) |
| locator := instance.NewLocator(repo, fetcher) |
|
|
| inst, _ := repo.Launch("default", "9868", true) |
|
|
| |
| locator.Register("tab_abc", inst.ID) |
|
|
| found, err := locator.FindInstanceByTabID("tab_abc") |
| if err != nil { |
| t.Fatal(err) |
| } |
| if found.ID != inst.ID { |
| t.Errorf("expected %s, got %s", inst.ID, found.ID) |
| } |
| } |
|
|
| func TestLocator_CacheMiss_QueriesBridges(t *testing.T) { |
| launcher := newMockLauncher() |
| fetcher := newMockFetcher() |
| repo := instance.NewRepository(launcher) |
| locator := instance.NewLocator(repo, fetcher) |
|
|
| inst, _ := repo.Launch("default", "9868", true) |
|
|
| |
| fetcher.AddTab("9868", "tab_xyz", "https://pinchtab.com") |
|
|
| found, err := locator.FindInstanceByTabID("tab_xyz") |
| if err != nil { |
| t.Fatal(err) |
| } |
| if found.ID != inst.ID { |
| t.Errorf("expected %s, got %s", inst.ID, found.ID) |
| } |
|
|
| |
| if locator.CacheSize() != 1 { |
| t.Errorf("expected cache size 1, got %d", locator.CacheSize()) |
| } |
| } |
|
|
| func TestLocator_TabNotFound(t *testing.T) { |
| launcher := newMockLauncher() |
| fetcher := newMockFetcher() |
| repo := instance.NewRepository(launcher) |
| locator := instance.NewLocator(repo, fetcher) |
|
|
| _, _ = repo.Launch("default", "9868", true) |
| fetcher.AddTab("9868", "tab_abc", "https://pinchtab.com") |
|
|
| _, err := locator.FindInstanceByTabID("nonexistent") |
| if err == nil { |
| t.Error("expected error for nonexistent tab") |
| } |
| } |
|
|
| func TestLocator_UsesInstanceURLWhenPresent(t *testing.T) { |
| launcher := newMockLauncher() |
| fetcher := newMockFetcher() |
| repo := instance.NewRepository(launcher) |
| locator := instance.NewLocator(repo, fetcher) |
|
|
| inst, _ := repo.Launch("remote", "9868", true) |
| inst.URL = "https://bridge.example.com:9868" |
| repo.Add(inst) |
| fetcher.AddTabForURL("https://bridge.example.com:9868", "tab_remote", "https://pinchtab.com") |
|
|
| found, err := locator.FindInstanceByTabID("tab_remote") |
| if err != nil { |
| t.Fatal(err) |
| } |
| if found.ID != inst.ID { |
| t.Fatalf("expected %s, got %s", inst.ID, found.ID) |
| } |
| } |
|
|
| func TestLocator_InvalidateRemovesCacheEntry(t *testing.T) { |
| launcher := newMockLauncher() |
| fetcher := newMockFetcher() |
| repo := instance.NewRepository(launcher) |
| locator := instance.NewLocator(repo, fetcher) |
|
|
| inst, _ := repo.Launch("default", "9868", true) |
| locator.Register("tab_abc", inst.ID) |
|
|
| if locator.CacheSize() != 1 { |
| t.Fatal("expected cache size 1") |
| } |
|
|
| locator.Invalidate("tab_abc") |
| if locator.CacheSize() != 0 { |
| t.Error("expected cache size 0 after invalidate") |
| } |
| } |
|
|
| func TestLocator_InvalidateInstance_RemovesAllTabs(t *testing.T) { |
| launcher := newMockLauncher() |
| fetcher := newMockFetcher() |
| repo := instance.NewRepository(launcher) |
| locator := instance.NewLocator(repo, fetcher) |
|
|
| inst, _ := repo.Launch("default", "9868", true) |
| locator.Register("tab_1", inst.ID) |
| locator.Register("tab_2", inst.ID) |
| locator.Register("tab_3", inst.ID) |
|
|
| locator.InvalidateInstance(inst.ID) |
| if locator.CacheSize() != 0 { |
| t.Errorf("expected cache size 0, got %d", locator.CacheSize()) |
| } |
| } |
|
|
| func TestLocator_StaleCache_InstanceGone(t *testing.T) { |
| launcher := newMockLauncher() |
| fetcher := newMockFetcher() |
| repo := instance.NewRepository(launcher) |
| locator := instance.NewLocator(repo, fetcher) |
|
|
| inst, _ := repo.Launch("default", "9868", true) |
| locator.Register("tab_abc", inst.ID) |
|
|
| |
| repo.Remove(inst.ID) |
|
|
| |
| _, err := locator.FindInstanceByTabID("tab_abc") |
| if err == nil { |
| t.Error("expected error when instance is gone") |
| } |
| } |
|
|
| |
|
|
| func TestAllocator_FCFS(t *testing.T) { |
| launcher := newMockLauncher() |
| repo := instance.NewRepository(launcher) |
| policy := &allocation.FCFS{} |
| alloc := instance.NewAllocator(repo, policy) |
|
|
| _, _ = repo.Launch("prof1", "9868", true) |
|
|
| got, err := alloc.Allocate() |
| if err != nil { |
| t.Fatal(err) |
| } |
| if got.ProfileName != "prof1" { |
| t.Errorf("expected prof1, got %s", got.ProfileName) |
| } |
| } |
|
|
| func TestAllocator_RoundRobin(t *testing.T) { |
| launcher := newMockLauncher() |
| repo := instance.NewRepository(launcher) |
| policy := allocation.NewRoundRobin() |
| alloc := instance.NewAllocator(repo, policy) |
|
|
| _, _ = repo.Launch("prof1", "9868", true) |
| _, _ = repo.Launch("prof2", "9869", true) |
|
|
| |
| |
| for range 4 { |
| _, err := alloc.Allocate() |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| } |
|
|
| func TestAllocator_NoRunningInstances(t *testing.T) { |
| launcher := newMockLauncher() |
| repo := instance.NewRepository(launcher) |
| alloc := instance.NewAllocator(repo, &allocation.FCFS{}) |
|
|
| _, err := alloc.Allocate() |
| if err == nil { |
| t.Error("expected error with no running instances") |
| } |
| } |
|
|
| |
|
|
| func TestManager_DelegatesToComponents(t *testing.T) { |
| launcher := newMockLauncher() |
| fetcher := newMockFetcher() |
| mgr := instance.NewManager(launcher, fetcher, &allocation.FCFS{}) |
|
|
| |
| inst, err := mgr.Launch("default", "9868", true) |
| if err != nil { |
| t.Fatal(err) |
| } |
|
|
| |
| got, ok := mgr.Get(inst.ID) |
| if !ok || got.ID != inst.ID { |
| t.Error("Get should delegate to repo") |
| } |
|
|
| |
| list := mgr.List() |
| if len(list) != 1 { |
| t.Errorf("expected 1 instance, got %d", len(list)) |
| } |
|
|
| |
| mgr.RegisterTab("tab_abc", inst.ID) |
| found, err := mgr.FindInstanceByTabID("tab_abc") |
| if err != nil { |
| t.Fatal(err) |
| } |
| if found.ID != inst.ID { |
| t.Error("FindInstanceByTabID should delegate to locator") |
| } |
|
|
| |
| alloc, err := mgr.Allocate() |
| if err != nil { |
| t.Fatal(err) |
| } |
| if alloc.ProfileName != "default" { |
| t.Error("Allocate should delegate to allocator") |
| } |
|
|
| |
| if err := mgr.Stop(inst.ID); err != nil { |
| t.Fatal(err) |
| } |
| if _, ok := mgr.Get(inst.ID); ok { |
| t.Error("instance should be gone after stop") |
| } |
| } |
|
|
| func TestManager_StopInvalidatesTabCache(t *testing.T) { |
| launcher := newMockLauncher() |
| fetcher := newMockFetcher() |
| mgr := instance.NewManager(launcher, fetcher, nil) |
|
|
| inst, _ := mgr.Launch("default", "9868", true) |
| mgr.RegisterTab("tab_1", inst.ID) |
| mgr.RegisterTab("tab_2", inst.ID) |
|
|
| _ = mgr.Stop(inst.ID) |
|
|
| |
| _, err := mgr.FindInstanceByTabID("tab_1") |
| if err == nil { |
| t.Error("expected error: tab cache should be invalidated after stop") |
| } |
| } |
|
|