2026-07-22 14:57:27 +08:00
|
|
|
package browser
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"sync"
|
|
|
|
|
"testing"
|
2026-07-22 15:03:21 +08:00
|
|
|
"time"
|
2026-07-22 14:57:27 +08:00
|
|
|
|
|
|
|
|
"chub/internal/domain"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestLauncherSeparatesProfilesBlocksDuplicatesAndReleasesAfterExit(t *testing.T) {
|
|
|
|
|
runner := &fakeRunner{}
|
|
|
|
|
launcher := NewLauncher(runner)
|
|
|
|
|
firstProfile := filepath.Join(t.TempDir(), "one")
|
|
|
|
|
secondProfile := filepath.Join(t.TempDir(), "two")
|
|
|
|
|
first, err := launcher.Start(context.Background(), launchSpec(firstProfile))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("first Start() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
second, err := launcher.Start(context.Background(), launchSpec(secondProfile))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("second Start() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if first.PID() == second.PID() {
|
|
|
|
|
t.Fatalf("separate profiles shared PID %d", first.PID())
|
|
|
|
|
}
|
|
|
|
|
if _, err := launcher.Start(context.Background(), launchSpec(firstProfile)); !errors.Is(err, domain.ErrProfileOccupied) {
|
|
|
|
|
t.Fatalf("duplicate Start() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
use, err := launcher.InspectProfile(context.Background(), firstProfile)
|
|
|
|
|
if err != nil || !use.Occupied || use.PID != first.PID() || use.Source != "chub_registry" {
|
|
|
|
|
t.Fatalf("profile use = %+v, error = %v", use, err)
|
|
|
|
|
}
|
|
|
|
|
runner.processAt(0).finish(0, nil)
|
|
|
|
|
if code, err := first.Wait(context.Background()); err != nil || code != 0 {
|
|
|
|
|
t.Fatalf("Wait() = %d, %v", code, err)
|
|
|
|
|
}
|
|
|
|
|
for i := 0; i < 50; i++ {
|
|
|
|
|
use, err = launcher.InspectProfile(context.Background(), firstProfile)
|
|
|
|
|
if err == nil && !use.Occupied {
|
|
|
|
|
break
|
|
|
|
|
}
|
2026-07-22 15:03:21 +08:00
|
|
|
time.Sleep(time.Millisecond)
|
2026-07-22 14:57:27 +08:00
|
|
|
}
|
|
|
|
|
if use.Occupied {
|
|
|
|
|
t.Fatalf("profile remained occupied after exit: %+v", use)
|
|
|
|
|
}
|
|
|
|
|
runner.processAt(1).finish(0, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestProcessWaitCancellationDoesNotTerminateBrowser(t *testing.T) {
|
|
|
|
|
runner := &fakeRunner{}
|
|
|
|
|
launcher := NewLauncher(runner)
|
|
|
|
|
handle, err := launcher.Start(context.Background(), launchSpec(filepath.Join(t.TempDir(), "profile")))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Start() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
cancel()
|
|
|
|
|
if _, err := handle.Wait(ctx); !errors.Is(err, context.Canceled) {
|
|
|
|
|
t.Fatalf("cancelled Wait() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if runner.processAt(0).wasFinished() {
|
|
|
|
|
t.Fatal("cancelled Wait terminated browser")
|
|
|
|
|
}
|
|
|
|
|
runner.processAt(0).finish(23, nil)
|
|
|
|
|
if code, err := handle.Wait(context.Background()); err != nil || code != 23 {
|
|
|
|
|
t.Fatalf("completed Wait() = %d, %v", code, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 15:03:21 +08:00
|
|
|
func TestLauncherStopProfileTargetsOnlyRegisteredProfile(t *testing.T) {
|
|
|
|
|
runner := &fakeRunner{}
|
|
|
|
|
launcher := NewLauncher(runner)
|
|
|
|
|
profile := filepath.Join(t.TempDir(), "profile")
|
|
|
|
|
if _, err := launcher.Start(context.Background(), launchSpec(profile)); err != nil {
|
|
|
|
|
t.Fatalf("Start() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := launcher.StopProfile(context.Background(), profile, false); err != nil {
|
|
|
|
|
t.Fatalf("StopProfile() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if runner.processAt(0).stopCount != 1 || runner.processAt(0).stopForce {
|
|
|
|
|
t.Fatalf("stop state = count %d force %v", runner.processAt(0).stopCount, runner.processAt(0).stopForce)
|
|
|
|
|
}
|
|
|
|
|
if err := launcher.StopProfile(context.Background(), filepath.Join(t.TempDir(), "other"), true); !errors.Is(err, domain.ErrInstanceNotFound) {
|
|
|
|
|
t.Fatalf("unknown profile error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 14:57:27 +08:00
|
|
|
func launchSpec(profile string) domain.LaunchSpec {
|
|
|
|
|
return domain.LaunchSpec{Kind: domain.BrowserChrome, Executable: `C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe`, UserDataDir: profile, TargetURL: "https://example.com"}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type fakeRunner struct {
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
processes []*fakeProcess
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *fakeRunner) Start(_ string, _ []string) (RunnerProcess, error) {
|
|
|
|
|
r.mu.Lock()
|
|
|
|
|
defer r.mu.Unlock()
|
|
|
|
|
p := newFakeProcess(4100 + len(r.processes))
|
|
|
|
|
r.processes = append(r.processes, p)
|
|
|
|
|
return p, nil
|
|
|
|
|
}
|
|
|
|
|
func (r *fakeRunner) processAt(index int) *fakeProcess {
|
|
|
|
|
r.mu.Lock()
|
|
|
|
|
defer r.mu.Unlock()
|
|
|
|
|
return r.processes[index]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type fakeProcess struct {
|
2026-07-22 15:03:21 +08:00
|
|
|
pid int
|
|
|
|
|
done chan processResult
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
finished bool
|
|
|
|
|
stopCount int
|
|
|
|
|
stopForce bool
|
2026-07-22 14:57:27 +08:00
|
|
|
}
|
|
|
|
|
type processResult struct {
|
|
|
|
|
code int
|
|
|
|
|
err error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newFakeProcess(pid int) *fakeProcess {
|
|
|
|
|
return &fakeProcess{pid: pid, done: make(chan processResult, 1)}
|
|
|
|
|
}
|
|
|
|
|
func (p *fakeProcess) PID() int { return p.pid }
|
|
|
|
|
func (p *fakeProcess) Wait() (int, error) { result := <-p.done; return result.code, result.err }
|
2026-07-22 15:03:21 +08:00
|
|
|
func (p *fakeProcess) Stop(_ context.Context, force bool) error {
|
|
|
|
|
p.mu.Lock()
|
|
|
|
|
defer p.mu.Unlock()
|
|
|
|
|
p.stopCount++
|
|
|
|
|
p.stopForce = force
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-07-22 14:57:27 +08:00
|
|
|
func (p *fakeProcess) finish(code int, err error) {
|
|
|
|
|
p.mu.Lock()
|
|
|
|
|
defer p.mu.Unlock()
|
|
|
|
|
if p.finished {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
p.finished = true
|
|
|
|
|
p.done <- processResult{code: code, err: err}
|
|
|
|
|
}
|
|
|
|
|
func (p *fakeProcess) wasFinished() bool { p.mu.Lock(); defer p.mu.Unlock(); return p.finished }
|