Files
mrleo1nidandClaude Opus 5 277ab7af4a Профили серверов: несколько миров с раздельными настройками
Профиль — это отдельный сервер: свой мир, .ini, песочница, набор модов и
лимит памяти JVM. Project Zomboid различает их по -servername, поэтому
файлы профилей не пересекаются. Работает всегда один профиль: серверные
файлы, игровые порты и файл запуска JVM общие, так что переключение
разрешено только на остановленном сервере.

- config: список профилей вместо одиночных server_name/admin_password/
  java_memory; старые поля переносятся в первый профиль при загрузке;
- supervisor: запускает активный профиль и выставляет его -Xmx на каждом
  старте, потому что файл запуска общий;
- бэкапы: архив снимается с одного профиля (мир + его конфиги), имя
  профиля попадает в имя файла, ротация считается по профилям;
- API профилей: список, создание с копированием настроек из другого
  профиля, правка, удаление и переключение активного;
- панель: селектор профиля в шапке и вкладка со списком профилей.

Удаление профиля убирает его только из панели: мир и конфиги остаются на
диске, чтобы случайное нажатие не стоило сохранений.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 00:27:12 +03:00

162 lines
5.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
"os"
"path/filepath"
"strings"
"testing"
)
// TestLoadMigratesSingleServerConfig проверяет обновление с версии, где сервер
// был один: его настройки должны стать первым профилем, а старые поля —
// исчезнуть из файла.
func TestLoadMigratesSingleServerConfig(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yaml")
legacy := strings.Join([]string{
"listen: 127.0.0.1:8080",
"server_name: myworld",
"admin_password: hunter2",
"java_memory: 8g",
"extra_args:",
" - -debug",
}, "\n")
if err := os.WriteFile(path, []byte(legacy), 0o600); err != nil {
t.Fatal(err)
}
cfg, err := Load(path)
if err != nil {
t.Fatal(err)
}
if len(cfg.Profiles) != 1 {
t.Fatalf("получено %d профилей, ожидался 1: %+v", len(cfg.Profiles), cfg.Profiles)
}
p := cfg.Profiles[0]
if p.ID != "myworld" || p.AdminPassword != "hunter2" || p.JavaMemory != "8g" {
t.Errorf("настройки не перенесены в профиль: %+v", p)
}
if len(p.ExtraArgs) != 1 || p.ExtraArgs[0] != "-debug" {
t.Errorf("extra_args не перенесены: %v", p.ExtraArgs)
}
if cfg.Active != "myworld" {
t.Errorf("активный профиль = %q", cfg.Active)
}
// Файл должен быть переписан без устаревших полей.
raw, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
saved := string(raw)
for _, key := range []string{"server_name:", "admin_password:", "java_memory:", "extra_args:"} {
if strings.Contains(saved, "\n"+key) {
t.Errorf("в файле осталось устаревшее поле %s:\n%s", key, saved)
}
}
if !strings.Contains(saved, "active_profile: myworld") {
t.Errorf("активный профиль не записан:\n%s", saved)
}
}
func TestLoadCreatesDefaultProfile(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yaml")
cfg, err := Load(path)
if err != nil {
t.Fatal(err)
}
if len(cfg.Profiles) != 1 || cfg.Profiles[0].ID != "servertest" {
t.Fatalf("профиль по умолчанию не создан: %+v", cfg.Profiles)
}
if cfg.ActiveProfile().ID != "servertest" {
t.Errorf("активный профиль = %q", cfg.ActiveProfile().ID)
}
}
// TestLoadRepairsDanglingActive: активный профиль могли удалить руками из
// файла, панель не должна из-за этого падать.
func TestLoadRepairsDanglingActive(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yaml")
raw := strings.Join([]string{
"active_profile: удалённый",
"profiles:",
" - id: vanilla",
" title: Ванилла",
}, "\n")
if err := os.WriteFile(path, []byte(raw), 0o600); err != nil {
t.Fatal(err)
}
cfg, err := Load(path)
if err != nil {
t.Fatal(err)
}
if cfg.Active != "vanilla" {
t.Errorf("активный профиль = %q, ожидался vanilla", cfg.Active)
}
if cfg.Profiles[0].JavaMemory == "" {
t.Error("профилю без java_memory не подставлено умолчание")
}
}
func TestProfileCRUD(t *testing.T) {
cfg := Default()
if err := cfg.AddProfile(Profile{ID: "modded", Title: "С модами"}); err != nil {
t.Fatal(err)
}
if err := cfg.AddProfile(Profile{ID: "modded"}); err == nil {
t.Error("дубликат профиля принят")
}
if err := cfg.AddProfile(Profile{ID: "плохой id"}); err == nil {
t.Error("недопустимый идентификатор принят")
}
if err := cfg.DeleteProfile("servertest"); err == nil {
t.Error("активный профиль удалён без переключения")
}
if err := cfg.SetActive("modded"); err != nil {
t.Fatal(err)
}
if err := cfg.DeleteProfile("servertest"); err != nil {
t.Fatal(err)
}
if err := cfg.DeleteProfile("modded"); err == nil {
t.Error("удалён единственный оставшийся профиль")
}
if err := cfg.SetActive("несуществующий"); err == nil {
t.Error("активирован несуществующий профиль")
}
}
func TestUpdateProfileKeepsID(t *testing.T) {
cfg := Default()
err := cfg.UpdateProfile("servertest", Profile{ID: "другой", Title: "Переименован", JavaMemory: "16g"})
if err != nil {
t.Fatal(err)
}
p := cfg.ActiveProfile()
if p.ID != "servertest" {
t.Errorf("идентификатор профиля изменился: %q", p.ID)
}
if p.Title != "Переименован" || p.JavaMemory != "16g" {
t.Errorf("настройки не сохранены: %+v", p)
}
}
func TestProfilePaths(t *testing.T) {
cfg := Default()
cfg.ZomboidDir = filepath.Join("/home", "pz", "Zomboid")
if got := cfg.ServerConfigPathFor("modded"); filepath.Base(got) != "modded.ini" {
t.Errorf("конфиг профиля: %s", got)
}
if got := cfg.SandboxConfigPathFor("modded"); filepath.Base(got) != "modded_SandboxVars.lua" {
t.Errorf("песочница профиля: %s", got)
}
if got := cfg.WorldDirFor("modded"); filepath.Base(got) != "modded" ||
!strings.Contains(filepath.ToSlash(got), "Saves/Multiplayer") {
t.Errorf("мир профиля: %s", got)
}
}