Игра приводит путь к вложенным файлам анимаций в нижний регистр целиком, вместе с именем папки мода. На Linux такие файлы не находятся, и сервер сыплет в лог сотнями FileNotFoundException. Кнопка на вкладке «Моды» раскладывает рядом строчные симлинки: файлы модов не меняются, повторный запуск после докачки безопасен, а на файловых системах без различия регистра кнопка честно говорит, что делать нечего. Ожидание строки о готовности стало настройкой профиля со значением по умолчанию 30 минут вместо общих 15. Сборке на три сотни модов пятнадцати не хватает, и панель объявляла бы работающим сервер, который ещё никого не пустит. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
127 lines
4.4 KiB
Go
127 lines
4.4 KiB
Go
package casefix
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// content собирает дерево мастерской с файлами в разном регистре.
|
|
func content(t *testing.T) (root, dir string) {
|
|
t.Helper()
|
|
root = t.TempDir()
|
|
dir = filepath.Join(root, "steamapps", "workshop", "content", pzAppID,
|
|
"3722134990", "mods", "GunsOfMarz", "42.16", "media", "AnimSets")
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, name := range []string{"LoadShotgun_HB.xml", "aim_down.xml"} {
|
|
if err := os.WriteFile(filepath.Join(dir, name), []byte("<anim/>"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return root, dir
|
|
}
|
|
|
|
// Проверять создание ссылок есть смысл только там, где регистр вообще
|
|
// различается: на Windows и macOS чинить нечего.
|
|
func skipCaseInsensitive(t *testing.T, result Result) {
|
|
t.Helper()
|
|
if result.CaseInsensitive {
|
|
t.Skip("файловая система не различает регистр")
|
|
}
|
|
}
|
|
|
|
func TestApplyLinksUppercaseNames(t *testing.T) {
|
|
root, dir := content(t)
|
|
|
|
result, err := Apply(context.Background(), []string{root}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
skipCaseInsensitive(t, result)
|
|
if result.Created == 0 {
|
|
t.Fatalf("не создано ни одной ссылки: %+v", result)
|
|
}
|
|
|
|
// Ссылка нужна и на файл, и на каждую папку пути: игра опускает в нижний
|
|
// регистр путь целиком, включая имя мода.
|
|
link := filepath.Join(dir, "loadshotgun_hb.xml")
|
|
if data, err := os.ReadFile(link); err != nil || string(data) != "<anim/>" {
|
|
t.Fatalf("ссылка на файл не читается: %v", err)
|
|
}
|
|
modDir := filepath.Join(root, "steamapps", "workshop", "content", pzAppID,
|
|
"3722134990", "mods", "gunsofmarz")
|
|
if _, err := os.Stat(modDir); err != nil {
|
|
t.Fatalf("нет ссылки на папку мода: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(modDir, "42.16", "media", "animsets", "loadshotgun_hb.xml")); err != nil {
|
|
t.Fatalf("путь целиком строчными не собрался: %v", err)
|
|
}
|
|
}
|
|
|
|
// Повторный запуск после докачки модов не должен ничего ломать.
|
|
func TestApplyIsIdempotent(t *testing.T) {
|
|
root, _ := content(t)
|
|
|
|
first, err := Apply(context.Background(), []string{root}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
skipCaseInsensitive(t, first)
|
|
second, err := Apply(context.Background(), []string{root}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if second.Created != 0 {
|
|
t.Errorf("второй проход создал ссылки заново: %d", second.Created)
|
|
}
|
|
if second.Existing != first.Created {
|
|
t.Errorf("готовых ссылок %d, а создано было %d", second.Existing, first.Created)
|
|
}
|
|
}
|
|
|
|
// Имя, которое уже занято настоящим файлом, не трогаем.
|
|
func TestApplyKeepsExistingLowercaseFile(t *testing.T) {
|
|
root, dir := content(t)
|
|
path := filepath.Join(dir, "loadshotgun_hb.xml")
|
|
if err := os.WriteFile(path, []byte("свой файл"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := Apply(context.Background(), []string{root}, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil || string(data) != "свой файл" {
|
|
t.Fatalf("существующий файл перезаписан: %q, %v", data, err)
|
|
}
|
|
}
|
|
|
|
func TestApplyWithoutWorkshopDir(t *testing.T) {
|
|
if _, err := Apply(context.Background(), []string{t.TempDir()}, nil); err == nil {
|
|
t.Fatal("ожидалась ошибка про ненайденную папку модов")
|
|
}
|
|
}
|
|
|
|
// На Windows и macOS кнопка должна честно сказать, что чинить нечего, а не
|
|
// пересчитывать все имена как «уже готовые».
|
|
func TestApplyReportsCaseInsensitiveFilesystem(t *testing.T) {
|
|
root, dir := content(t)
|
|
if !caseInsensitive(dir) {
|
|
t.Skip("файловая система различает регистр")
|
|
}
|
|
|
|
result, err := Apply(context.Background(), []string{root}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !result.CaseInsensitive {
|
|
t.Error("нечувствительность к регистру не распознана")
|
|
}
|
|
if result.Created != 0 || result.Existing != 0 || result.Scanned != 0 {
|
|
t.Errorf("дерево обошли впустую: %+v", result)
|
|
}
|
|
}
|