Files

222 lines
6.6 KiB
Go

package instance
import (
"bufio"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
)
// TestAcquireSeparatesConcurrentInstances 验证槽位分配:第一个进程用默认数据目录,
// 第二个进程拿到 instance-2,锁释放后默认实例可被复用。
func TestAcquireSeparatesConcurrentInstances(t *testing.T) {
baseDir := t.TempDir()
t.Setenv("HUYA_DATA_DIR", baseDir)
t.Setenv("HUYA_LOG_DIR", "")
first, err := Acquire()
if err != nil {
t.Fatal(err)
}
defer first.Release()
if first.Name() != "默认实例" || first.ID() != "default" {
t.Fatalf("first instance = %q/%q", first.Name(), first.ID())
}
if first.Paths().DataDir != baseDir {
t.Fatalf("first dataDir = %q, want %q", first.Paths().DataDir, baseDir)
}
if first.Paths().LogDir != filepath.Join(baseDir, "logs") {
t.Fatalf("first logDir = %q", first.Paths().LogDir)
}
second, err := Acquire()
if err != nil {
t.Fatal(err)
}
defer second.Release()
wantSecondDir := filepath.Join(baseDir, "instances", "instance-2")
if second.Name() != "实例 2" || second.ID() != "instance-2" {
t.Fatalf("second instance = %q/%q", second.Name(), second.ID())
}
if second.Paths().DataDir != wantSecondDir {
t.Fatalf("second dataDir = %q, want %q", second.Paths().DataDir, wantSecondDir)
}
// 日志目录必须落在实例自己的目录里,否则多开时日志会交错。
if second.Paths().LogDir != filepath.Join(wantSecondDir, "logs") {
t.Fatalf("second logDir = %q", second.Paths().LogDir)
}
first.Release()
reused, err := Acquire()
if err != nil {
t.Fatal(err)
}
defer reused.Release()
if reused.Name() != "默认实例" || reused.Paths().DataDir != baseDir {
t.Fatalf("reused instance = %q/%q", reused.Name(), reused.Paths().DataDir)
}
}
// TestWebviewDirsAreIsolated WebView2 一个用户数据目录只能有一个浏览器进程,
// 每个实例必须是不同目录,否则第二个实例会偶发开不出窗口。
func TestWebviewDirsAreIsolated(t *testing.T) {
baseDir := t.TempDir()
t.Setenv("HUYA_DATA_DIR", baseDir)
t.Setenv("HUYA_LOG_DIR", "")
first, err := Acquire()
if err != nil {
t.Fatal(err)
}
defer first.Release()
second, err := Acquire()
if err != nil {
t.Fatal(err)
}
defer second.Release()
firstDir := first.WebviewUserDataDir()
secondDir := second.WebviewUserDataDir()
if firstDir == "" || secondDir == "" || firstDir == secondDir {
t.Fatalf("webview dirs = %q / %q", firstDir, secondDir)
}
if want := filepath.Join(baseDir, "webview2"); firstDir != want {
t.Fatalf("first webview dir = %q, want %q", firstDir, want)
}
if want := filepath.Join(baseDir, "instances", "instance-2", "webview2"); secondDir != want {
t.Fatalf("second webview dir = %q, want %q", secondDir, want)
}
}
// TestAcquireNeverSharesDefaultDir 保证实例占位失败时也不会退回默认目录。
// 退回默认目录会让两个进程共享日志文件与前端数据目录。
func TestAcquireNeverSharesDefaultDir(t *testing.T) {
baseDir := t.TempDir()
// 用一个普通文件占住 <dataDir>,让 <dataDir>/instances/.locks 无法创建。
blocked := filepath.Join(baseDir, "blocked")
if err := os.WriteFile(blocked, []byte("x"), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("HUYA_DATA_DIR", blocked)
t.Setenv("HUYA_LOG_DIR", "")
lease, err := Acquire()
if lease == nil {
t.Fatal("实例占位必须始终返回可用目录")
}
defer lease.Release()
if err == nil {
t.Fatal("降级路径应当把原因返回给上层记录")
}
if lease.Name() != "临时实例" {
t.Fatalf("降级实例名 = %q, want 临时实例", lease.Name())
}
if lease.Paths().DataDir == blocked || !strings.Contains(lease.ID(), "instance-tmp-") {
t.Fatalf("降级目录必须与本进程绑定且独立: %q", lease.Paths().DataDir)
}
}
// TestReleaseIsIdempotent 验证重复释放不会出错(Shutdown 与 defer 可能都调用一次)。
func TestReleaseIsIdempotent(t *testing.T) {
t.Setenv("HUYA_DATA_DIR", t.TempDir())
t.Setenv("HUYA_LOG_DIR", "")
lease, err := Acquire()
if err != nil {
t.Fatal(err)
}
lease.Release()
lease.Release()
}
// TestAcquireIsExclusiveAcrossProcesses 用两个真实进程验证「多开」:第二个进程不会拿到
// 与第一个进程相同的数据目录(否则会互相覆盖日志与前端数据)。
func TestAcquireIsExclusiveAcrossProcesses(t *testing.T) {
if os.Getenv("HUYA_INSTANCE_HELPER") == "1" {
runInstanceHelper()
return
}
baseDir := t.TempDir()
first := startInstanceHelper(t, baseDir)
defer first.stop()
firstName, firstID, firstDir := first.read(t)
// 第一个进程已经拿到锁之后再启动第二个,避免出现「同时抢同一槽位」的随机性。
second := startInstanceHelper(t, baseDir)
defer second.stop()
_, secondID, secondDir := second.read(t)
if firstID != "default" || firstName != "默认实例" {
t.Fatalf("第一个进程实例 = %q/%q, want 默认实例/default", firstName, firstID)
}
if secondID != "instance-2" {
t.Fatalf("第二个进程实例 = %q, want instance-2", secondID)
}
if firstDir == secondDir {
t.Fatalf("两个进程的数据目录相同: %s", firstDir)
}
if !strings.Contains(secondDir, filepath.Join("instances", "instance-2")) {
t.Fatalf("第二个进程数据目录 = %q", secondDir)
}
}
// runInstanceHelper 是子进程分支:占位后打印实例信息,短暂保持占用再释放。
func runInstanceHelper() {
lease, err := Acquire()
if err != nil {
fmt.Println("ERR:" + err.Error())
return
}
fmt.Printf("%s|%s|%s\n", lease.Name(), lease.ID(), lease.Paths().DataDir)
time.Sleep(2 * time.Second)
lease.Release()
}
type instanceHelper struct {
cmd *exec.Cmd
lines *bufio.Scanner
}
func startInstanceHelper(t *testing.T, baseDir string) *instanceHelper {
t.Helper()
cmd := exec.Command(os.Args[0], "-test.run=TestAcquireIsExclusiveAcrossProcesses")
cmd.Env = append(os.Environ(),
"HUYA_INSTANCE_HELPER=1",
"HUYA_DATA_DIR="+baseDir,
"HUYA_LOG_DIR=",
)
stdout, err := cmd.StdoutPipe()
if err != nil {
t.Fatal(err)
}
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
return &instanceHelper{cmd: cmd, lines: bufio.NewScanner(stdout)}
}
// read 读取子进程输出的 "名称|标识|数据目录"。
func (h *instanceHelper) read(t *testing.T) (name, id, dir string) {
t.Helper()
if !h.lines.Scan() {
t.Fatalf("子进程未输出实例信息: %v", h.lines.Err())
}
parts := strings.SplitN(strings.TrimSpace(h.lines.Text()), "|", 3)
if len(parts) != 3 {
t.Fatalf("子进程输出异常: %q", h.lines.Text())
}
return parts[0], parts[1], parts[2]
}
func (h *instanceHelper) stop() {
if h.cmd.Process != nil {
_ = h.cmd.Wait()
}
}