package proot import ( "encoding/json" "os" "path/filepath" "testing" "github.com/armoredgate/volt/pkg/backend" "gopkg.in/yaml.v3" ) func TestName(t *testing.T) { b := New() if b.Name() != "proot" { t.Errorf("expected name 'proot', got %q", b.Name()) } } func TestCapabilities(t *testing.T) { b := New() if b.SupportsVMs() { t.Error("proot should not support VMs") } if b.SupportsServices() { t.Error("proot should not support services") } if !b.SupportsNetworking() { t.Error("proot should support basic networking") } if b.SupportsTuning() { t.Error("proot should not support tuning") } } func TestInit(t *testing.T) { tmpDir := t.TempDir() b := New() if err := b.Init(tmpDir); err != nil { t.Fatalf("Init failed: %v", err) } // Verify directory structure for _, sub := range []string{"containers", "images", "tmp"} { path := filepath.Join(tmpDir, sub) info, err := os.Stat(path) if err != nil { t.Errorf("expected directory %s to exist: %v", sub, err) continue } if !info.IsDir() { t.Errorf("expected %s to be a directory", sub) } } // Verify tmp has 0777 permissions info, _ := os.Stat(filepath.Join(tmpDir, "tmp")) if info.Mode().Perm() != 0777 { t.Errorf("expected tmp perms 0777, got %o", info.Mode().Perm()) } } func TestCreateAndDelete(t *testing.T) { tmpDir := t.TempDir() b := New() b.Init(tmpDir) // Create a container opts := backend.CreateOptions{ Name: "test-container", Memory: "512M", CPU: 1, Env: []string{"FOO=bar"}, Ports: []backend.PortMapping{{HostPort: 8080, ContainerPort: 80, Protocol: "tcp"}}, } if err := b.Create(opts); err != nil { t.Fatalf("Create failed: %v", err) } // Verify container directory structure cDir := filepath.Join(tmpDir, "containers", "test-container") for _, sub := range []string{"rootfs", "logs"} { path := filepath.Join(cDir, sub) if _, err := os.Stat(path); err != nil { t.Errorf("expected %s to exist: %v", sub, err) } } // Verify state.json stateData, err := os.ReadFile(filepath.Join(cDir, "state.json")) if err != nil { t.Fatalf("failed to read state.json: %v", err) } var state containerState if err := json.Unmarshal(stateData, &state); err != nil { t.Fatalf("failed to parse state.json: %v", err) } if state.Name != "test-container" { t.Errorf("expected name 'test-container', got %q", state.Name) } if state.Status != "created" { t.Errorf("expected status 'created', got %q", state.Status) } // Verify config.yaml cfgData, err := os.ReadFile(filepath.Join(cDir, "config.yaml")) if err != nil { t.Fatalf("failed to read config.yaml: %v", err) } var cfg containerConfig if err := yaml.Unmarshal(cfgData, &cfg); err != nil { t.Fatalf("failed to parse config.yaml: %v", err) } if cfg.Memory != "512M" { t.Errorf("expected memory '512M', got %q", cfg.Memory) } if len(cfg.Ports) != 1 || cfg.Ports[0].HostPort != 8080 { t.Errorf("expected port mapping 8080:80, got %+v", cfg.Ports) } // Verify duplicate create fails if err := b.Create(opts); err == nil { t.Error("expected duplicate create to fail") } // List should return one container containers, err := b.List() if err != nil { t.Fatalf("List failed: %v", err) } if len(containers) != 1 { t.Errorf("expected 1 container, got %d", len(containers)) } // Inspect should work info, err := b.Inspect("test-container") if err != nil { t.Fatalf("Inspect failed: %v", err) } if info.Status != "created" { t.Errorf("expected status 'created', got %q", info.Status) } // Delete should work if err := b.Delete("test-container", false); err != nil { t.Fatalf("Delete failed: %v", err) } // Verify directory removed if _, err := os.Stat(cDir); !os.IsNotExist(err) { t.Error("expected container directory to be removed") } // List should be empty now containers, err = b.List() if err != nil { t.Fatalf("List failed: %v", err) } if len(containers) != 0 { t.Errorf("expected 0 containers, got %d", len(containers)) } } func TestCopyOperations(t *testing.T) { tmpDir := t.TempDir() b := New() b.Init(tmpDir) // Create a container opts := backend.CreateOptions{Name: "copy-test"} if err := b.Create(opts); err != nil { t.Fatalf("Create failed: %v", err) } // Create a source file on "host" srcFile := filepath.Join(tmpDir, "host-file.txt") os.WriteFile(srcFile, []byte("hello from host"), 0644) // Copy to container if err := b.CopyToContainer("copy-test", srcFile, "/etc/test.txt"); err != nil { t.Fatalf("CopyToContainer failed: %v", err) } // Verify file exists in rootfs containerFile := filepath.Join(tmpDir, "containers", "copy-test", "rootfs", "etc", "test.txt") data, err := os.ReadFile(containerFile) if err != nil { t.Fatalf("file not found in container: %v", err) } if string(data) != "hello from host" { t.Errorf("expected 'hello from host', got %q", string(data)) } // Copy from container dstFile := filepath.Join(tmpDir, "from-container.txt") if err := b.CopyFromContainer("copy-test", "/etc/test.txt", dstFile); err != nil { t.Fatalf("CopyFromContainer failed: %v", err) } data, err = os.ReadFile(dstFile) if err != nil { t.Fatalf("failed to read copied file: %v", err) } if string(data) != "hello from host" { t.Errorf("expected 'hello from host', got %q", string(data)) } } func TestLogs(t *testing.T) { tmpDir := t.TempDir() b := New() b.Init(tmpDir) // Create a container opts := backend.CreateOptions{Name: "log-test"} b.Create(opts) // Write some log lines logDir := filepath.Join(tmpDir, "containers", "log-test", "logs") logFile := filepath.Join(logDir, "current.log") lines := "line1\nline2\nline3\nline4\nline5\n" os.WriteFile(logFile, []byte(lines), 0644) // Full logs content, err := b.Logs("log-test", backend.LogOptions{}) if err != nil { t.Fatalf("Logs failed: %v", err) } if content != lines { t.Errorf("expected full log content, got %q", content) } // Tail 2 lines content, err = b.Logs("log-test", backend.LogOptions{Tail: 2}) if err != nil { t.Fatalf("Logs tail failed: %v", err) } // Last 2 lines of "line1\nline2\nline3\nline4\nline5\n" split gives 6 elements // (last is empty after trailing \n), so tail 2 gives "line5\n" if content == "" { t.Error("expected some tail output") } // No logs available content, err = b.Logs("nonexistent", backend.LogOptions{}) if err == nil { // Container doesn't exist, should get error from readState // but Logs reads file directly, so check } } func TestAvailable(t *testing.T) { b := New() // Just verify it doesn't panic _ = b.Available() } func TestProcessAlive(t *testing.T) { // PID 1 (init) should be alive if !processAlive(1) { t.Error("expected PID 1 to be alive") } // PID 0 should not be alive if processAlive(0) { t.Error("expected PID 0 to not be alive") } // Very large PID should not be alive if processAlive(999999999) { t.Error("expected PID 999999999 to not be alive") } } func TestDetectOS(t *testing.T) { tmpDir := t.TempDir() // No os-release file result := detectOS(tmpDir) if result != "-" { t.Errorf("expected '-' for missing os-release, got %q", result) } // Create os-release etcDir := filepath.Join(tmpDir, "etc") os.MkdirAll(etcDir, 0755) osRelease := `NAME="Ubuntu" VERSION="24.04 LTS (Noble Numbat)" ID=ubuntu PRETTY_NAME="Ubuntu 24.04 LTS" VERSION_ID="24.04" ` os.WriteFile(filepath.Join(etcDir, "os-release"), []byte(osRelease), 0644) result = detectOS(tmpDir) if result != "Ubuntu 24.04 LTS" { t.Errorf("expected 'Ubuntu 24.04 LTS', got %q", result) } } func TestEntrypointDetection(t *testing.T) { tmpDir := t.TempDir() b := New() cfg := &containerConfig{Name: "test"} // Empty rootfs — should fallback to /bin/sh ep, args := b.detectEntrypoint(tmpDir, cfg) if ep != "/bin/sh" { t.Errorf("expected /bin/sh fallback, got %q", ep) } if len(args) != 0 { t.Errorf("expected no args for /bin/sh, got %v", args) } // Create /init initPath := filepath.Join(tmpDir, "init") os.WriteFile(initPath, []byte("#!/bin/sh\nexec /bin/sh"), 0755) ep, _ = b.detectEntrypoint(tmpDir, cfg) if ep != "/init" { t.Errorf("expected /init, got %q", ep) } // Remove /init, create nginx os.Remove(initPath) nginxDir := filepath.Join(tmpDir, "usr", "sbin") os.MkdirAll(nginxDir, 0755) os.WriteFile(filepath.Join(nginxDir, "nginx"), []byte(""), 0755) ep, args = b.detectEntrypoint(tmpDir, cfg) if ep != "/usr/sbin/nginx" { t.Errorf("expected /usr/sbin/nginx, got %q", ep) } // With port mapping, should use shell wrapper cfg.Ports = []backend.PortMapping{{HostPort: 8080, ContainerPort: 80}} ep, args = b.detectEntrypoint(tmpDir, cfg) if ep != "/bin/sh" { t.Errorf("expected /bin/sh wrapper for nginx with ports, got %q", ep) } if len(args) != 2 || args[0] != "-c" { t.Errorf("expected [-c ] for nginx wrapper, got %v", args) } }