package vmm
import (
"context"
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/openeuler/Conch/internal/config"
"github.com/openeuler/Conch/internal/vmm/driver"
)
func TestSandboxSocketPathUsesShortStableName(t *testing.T) {
oldWorkDir := config.WorkDir
t.Cleanup(func() { config.WorkDir = oldWorkDir })
config.WorkDir = filepath.Join(os.TempDir(), "conch-socket-test-"+strings.Repeat("a", 45))
t.Cleanup(func() { _ = os.RemoveAll(config.WorkDir) })
sandboxID := "sandbox-" + strings.Repeat("very-long-id-", 12)
got, err := SandboxSocketPath("x", sandboxID)
if err != nil {
t.Fatalf("SandboxSocketPath() error = %v", err)
}
if len(got) > unixSocketPathMax {
t.Fatalf("socket path length = %d, want <= %d: %s", len(got), unixSocketPathMax, got)
}
if strings.Contains(got, sandboxID) {
t.Fatalf("socket path still embeds sandbox id: %s", got)
}
again, err := SandboxSocketPath("x", sandboxID)
if err != nil {
t.Fatalf("SandboxSocketPath() second error = %v", err)
}
if got != again {
t.Fatalf("SandboxSocketPath() is not stable: %s vs %s", got, again)
}
}
func TestSandboxSocketPathRejectsTooLongWorkDir(t *testing.T) {
oldWorkDir := config.WorkDir
t.Cleanup(func() { config.WorkDir = oldWorkDir })
config.WorkDir = filepath.Join(os.TempDir(), "conch-socket-test-"+strings.Repeat("a", 110))
t.Cleanup(func() { _ = os.RemoveAll(config.WorkDir) })
if _, err := SandboxSocketPath("x", "sandbox"); err == nil {
t.Fatalf("SandboxSocketPath() error = nil, want path length error")
}
}
func TestIsSandboxSocketName(t *testing.T) {
for _, name := range []string{
"0123456789abcdef.sock",
"0123456789abcdef.sock.serial",
} {
if !sandboxSocketNameRE.MatchString(name) {
t.Fatalf("sandboxSocketNameRE.MatchString(%q) = false, want true", name)
}
}
for _, name := range []string{
"sandbox.sock",
"0123456789abcdef.serial",
"0123456789abcdef.sock.bak",
"0123456789ABCDEf.sock",
} {
if sandboxSocketNameRE.MatchString(name) {
t.Fatalf("sandboxSocketNameRE.MatchString(%q) = true, want false", name)
}
}
}
func TestKillStaleVMMProcessIgnoresReusedPID(t *testing.T) {
binaries := map[string]string{"test-vmm": "/opt/test/vmm"}
if err := killStaleVMMProcess(os.Getpid(), binaries); err != nil {
t.Fatalf("killStaleVMMProcess() error = %v, want nil for non-VMM process", err)
}
}
func TestMatchesConfiguredVMMCommand(t *testing.T) {
binaries := map[string]string{
"vmm-a": "/opt/test/vmm-a",
"vmm-b": "/opt/test/vmm-b",
}
tests := []struct {
name string
cmdline string
want bool
}{
{name: "configured binary", cmdline: "/opt/test/vmm-a\x00--api-socket\x00socket", want: true},
{name: "second configured binary", cmdline: "/opt/test/vmm-b\x00--restore", want: true},
{name: "binary only in argument", cmdline: "/usr/bin/wrapper\x00/opt/test/vmm-a", want: false},
{name: "path prefix", cmdline: "/opt/test/vmm-a.backup\x00--run", want: false},
{name: "empty command line", cmdline: "", want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := matchesConfiguredVMMCommand([]byte(tt.cmdline), binaries); got != tt.want {
t.Fatalf("matchesConfiguredVMMCommand(%q) = %t, want %t", tt.cmdline, got, tt.want)
}
})
}
}
type blockingDaemonClient struct {
release chan struct{}
cleanupCalls atomic.Int32
deleteCalls atomic.Int32
}
func (c *blockingDaemonClient) BuildStartCmd(*ResourceArgs, bool) (string, error) { return "", nil }
func (c *blockingDaemonClient) CheckAgentAlive(ctx context.Context, processExited driver.ProcessExit) error {
select {
case <-c.release:
return nil
case <-processExited.Done():
if waitErr := processExited.Err(); waitErr == nil {
return errors.New("vmm process exited before conch-init became ready")
} else {
return errors.Join(errors.New("vmm process exited before conch-init became ready"), waitErr)
}
case <-ctx.Done():
return ctx.Err()
}
}
func (c *blockingDaemonClient) PauseVM() error { return nil }
func (c *blockingDaemonClient) ResumeVM() error { return nil }
func (c *blockingDaemonClient) DeleteVM() error { c.deleteCalls.Add(1); return nil }
func (c *blockingDaemonClient) CreateSnapshot(string) error { return nil }
func (c *blockingDaemonClient) LoadSnapshot(string, bool) error { return nil }
func (c *blockingDaemonClient) PrepareLaunch(*ResourceArgs, bool) error {
return nil
}
func (c *blockingDaemonClient) AfterProcessStart() {}
func (c *blockingDaemonClient) WaitForCreateReady(context.Context, driver.ProcessExit) error {
return nil
}
func (c *blockingDaemonClient) WaitForRestoreReady(context.Context, driver.ProcessExit) error {
return nil
}
func (c *blockingDaemonClient) Cleanup() { c.cleanupCalls.Add(1) }
func TestStopIgnoresProcessDoneWhenProcessAlreadyFinished(t *testing.T) {
cmd := exec.Command("true")
if err := cmd.Start(); err != nil {
t.Fatalf("start process: %v", err)
}
if err := cmd.Wait(); err != nil {
t.Fatalf("wait for process: %v", err)
}
client := &blockingDaemonClient{release: make(chan struct{})}
process := &Process{
cmd: cmd,
adapter: client,
exitDone: make(chan struct{}),
}
if err := process.Stop(context.Background()); err != nil {
t.Fatalf("Stop() error = %v, want nil", err)
}
if got := client.cleanupCalls.Load(); got != 1 {
t.Fatalf("Cleanup() calls = %d, want 1", got)
}
}
func TestWaitForAgentAliveReturnsProcessExitError(t *testing.T) {
processErr := errors.New("stratovirt exited after creating qmp socket")
client := &blockingDaemonClient{release: make(chan struct{})}
process := &Process{
adapter: client,
exitDone: make(chan struct{}),
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
process.recordExit(processErr)
t.Cleanup(func() { close(client.release) })
err := process.waitForAgentAlive(ctx)
if !errors.Is(err, processErr) {
t.Fatalf("waitForAgentAlive() error = %v, want %v", err, processErr)
}
if !strings.Contains(err.Error(), "exited before conch-init became ready") {
t.Fatalf("waitForAgentAlive() error = %q, want early exit context", err.Error())
}
}
func TestStopRetainsResourcesUntilExit(t *testing.T) {
cmd := exec.Command("sleep", "30")
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
defer func() { _ = cmd.Process.Kill(); _ = cmd.Wait() }()
client := &blockingDaemonClient{}
socket := filepath.Join(t.TempDir(), "control")
if err := os.WriteFile(socket, nil, 0600); err != nil {
t.Fatal(err)
}
process := &Process{cmd: cmd, adapter: client, exitDone: make(chan struct{}), apiReady: true, VmmSocketPath: socket}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if err := process.Stop(ctx); !errors.Is(err, context.Canceled) {
t.Fatal(err)
}
if client.deleteCalls.Load() != 1 {
t.Fatal("stop did not call control API")
}
if client.cleanupCalls.Load() != 0 {
t.Fatal("cleaned resources before exit")
}
close(process.exitDone)
if err := process.Stop(context.Background()); err != nil {
t.Fatal(err)
}
if client.cleanupCalls.Load() != 1 {
t.Fatal("resources were not cleaned after exit")
}
}