Files
90CNServer/pvf/services/archive_diff_merge_test.go
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

345 lines
10 KiB
Go
Raw 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.
// Author: flobit
package services
import (
"path/filepath"
"strings"
"testing"
"time"
"pvfine/internal/pvf"
)
func TestArchiveDiffStringPoolDifferenceAndMerge(t *testing.T) {
// 创建基础归档 A
dir := t.TempDir()
pathA := filepath.Join(dir, "base.pvf")
pathB := filepath.Join(dir, "other.pvf")
a := pvf.New()
// 写入脚本,引用 A 中的字符串
if _, err := a.AddFileText("etc/test.etc", "[name]\n`hello_from_A`\n", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if _, err := a.AddFileText("etc/same.etc", "[name]\n`common_string`\n", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if err := a.SaveAs(pathA); err != nil {
t.Fatal(err)
}
// 创建对比归档 B,字符串池内容不同
b := pvf.New()
if _, err := b.AddFileText("etc/test.etc", "[name]\n`hello_from_B`\n", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if _, err := b.AddFileText("etc/same.etc", "[name]\n`common_string`\n", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if _, err := b.AddFileText("etc/added.etc", "[name]\n`newly_added`\n", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if err := b.SaveAs(pathB); err != nil {
t.Fatal(err)
}
core := NewCore()
openA, err := pvf.Open(pathA)
if err != nil {
t.Fatal(err)
}
if err := core.setArchive(openA); err != nil {
t.Fatal(err)
}
defer core.closeArchive()
diffService := NewArchiveDiffService(core)
status, err := diffService.Start(ArchiveDiffRequest{Path: pathB})
if err != nil {
t.Fatalf("Start diff failed: %v", err)
}
// 等待比较完成
var finalStatus *ArchiveDiffStatus
for i := 0; i < 50; i++ {
s, err := diffService.Status(status.ID)
if err != nil {
t.Fatal(err)
}
if s.State == "ready" {
finalStatus = s
break
}
if s.State == "error" {
t.Fatalf("diff failed with error: %s", s.Error)
}
time.Sleep(20 * time.Millisecond)
}
if finalStatus == nil {
t.Fatal("diff timed out")
}
// 验证差异结果:
// 默认方向下 other(B) 为 before,current(A) 为 after:
// etc/test.etc 因字符串池不同识别为 Modified;
// etc/added.etc 在 B 中存在而在 A 中不存在,在默认方向下标记为 Deleted;
// etc/same.etc 内容相同,不出现在差异中。
if finalStatus.Modified != 1 {
t.Fatalf("Modified count = %d, want 1", finalStatus.Modified)
}
if finalStatus.Deleted != 1 {
t.Fatalf("Deleted count = %d, want 1", finalStatus.Deleted)
}
// 验证 FlatFiles 全量与关键字过滤
flatAll, err := diffService.FlatFiles(status.ID, "", "all")
if err != nil {
t.Fatal(err)
}
if len(flatAll) != 2 {
t.Fatalf("FlatFiles count = %d, want 2", len(flatAll))
}
flatFilteredTest, err := diffService.FlatFiles(status.ID, "test", "all")
if err != nil {
t.Fatal(err)
}
if len(flatFilteredTest) != 1 || flatFilteredTest[0].Path != "etc/test.etc" || flatFilteredTest[0].Name != "test.etc" {
t.Fatalf("FlatFiles filtered by 'test' unexpected: %+v", flatFilteredTest)
}
flatNone, err := diffService.FlatFiles(status.ID, "nonexistent_query", "all")
if err != nil {
t.Fatal(err)
}
if len(flatNone) != 0 {
t.Fatalf("FlatFiles filtered by nonexistent should be empty, got: %+v", flatNone)
}
// 验证 FilteredChildren
allChildren, err := diffService.FilteredChildren(status.ID, "", "all")
if err != nil || len(allChildren) != 1 || allChildren[0].Name != "etc" || allChildren[0].Count != 2 {
t.Fatalf("FilteredChildren all failed: %+v", allChildren)
}
modChildren, err := diffService.FilteredChildren(status.ID, "", "modify")
if err != nil || len(modChildren) != 1 || modChildren[0].Name != "etc" || modChildren[0].Count != 1 {
t.Fatalf("FilteredChildren modify failed: %+v", modChildren)
}
delChildren, err := diffService.FilteredChildren(status.ID, "", "delete")
if err != nil || len(delChildren) != 1 || delChildren[0].Name != "etc" || delChildren[0].Count != 1 {
t.Fatalf("FilteredChildren delete failed: %+v", delChildren)
}
addChildren, err := diffService.FilteredChildren(status.ID, "", "add")
if err != nil || len(addChildren) != 0 {
t.Fatalf("FilteredChildren add should be empty, got: %+v", addChildren)
}
// 测试 MergeFiles:将 B 中的 etc/test.etc 合并到 A
res, err := diffService.MergeFiles(status.ID, []string{"etc/test.etc"})
if err != nil {
t.Fatalf("MergeFiles failed: %v", err)
}
if res.SuccessCount != 1 || len(res.MergedPaths) != 1 {
t.Fatalf("Merge result unexpected: %+v", res)
}
// 验证 A 中的内容已经被成功更新为 B 中的字符串
idx, ok := core.archive.Find("etc/test.etc")
if !ok {
t.Fatal("etc/test.etc not found in archive A after merge")
}
mergedText, err := core.archive.CanonicalText(idx)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(mergedText, "hello_from_B") {
t.Fatalf("merged text = %q, want hello_from_B", mergedText)
}
// 测试 MergeText:修改单文件文本(仍与 B 有差异)
newContent := "[name]\n\t`manually_edited`\n"
if err := diffService.MergeText(status.ID, "etc/test.etc", newContent); err != nil {
t.Fatalf("MergeText failed: %v", err)
}
files, err := diffService.FlatFiles(status.ID, "test.etc", "modify")
if err != nil || len(files) != 1 {
t.Fatalf("re-edited merged file missing from diff: files=%+v err=%v", files, err)
}
detail, err := diffService.FileDiff(status.ID, "etc/test.etc")
if err != nil || detail.Operation != "modify" || !strings.Contains(detail.AfterText, "manually_edited") {
t.Fatalf("re-edited file detail: %+v err=%v", detail, err)
}
updated, err := diffService.Status(status.ID)
if err != nil || updated.Modified != 1 {
t.Fatalf("re-edited file statistics: %+v err=%v", updated, err)
}
exports := NewExportService(core)
preview, err := exports.Prepare(ExportRequest{Source: "archive-diff", DiffID: status.ID, Mode: "direct", Scopes: []string{"etc/test.etc"}})
if err != nil {
t.Fatal(err)
}
root, err := exports.executeTo(preview.ID, t.TempDir())
if err != nil {
t.Fatal(err)
}
if text := readExportFile(t, root, "etc/test.etc"); !strings.Contains(text, "manually_edited") {
t.Fatalf("export lost re-edited merged file: %q", text)
}
// 测试通过目录路径合并整个目录下的所有差异文件(包含仍有剩余差异的 test.etc 与未合并的 added.etc)
dirMergeRes, err := diffService.MergeFiles(status.ID, []string{"etc"})
if err != nil {
t.Fatalf("MergeFiles by directory failed: %v", err)
}
if dirMergeRes.SuccessCount != 2 {
t.Fatalf("Directory merge expected 2 remaining files merged, got: %+v", dirMergeRes)
}
}
func TestArchiveDiffPartialSavePreservesRemainingDiff(t *testing.T) {
dir := t.TempDir()
pathA := filepath.Join(dir, "base.pvf")
pathB := filepath.Join(dir, "other.pvf")
a := pvf.New()
if _, err := a.AddFileText("etc/test.etc", "[name]\n`val_A`\n[price]\n`100`\n", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if err := a.SaveAs(pathA); err != nil {
t.Fatal(err)
}
b := pvf.New()
if _, err := b.AddFileText("etc/test.etc", "[name]\n`val_B`\n[price]\n`200`\n", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if err := b.SaveAs(pathB); err != nil {
t.Fatal(err)
}
core := NewCore()
openA, err := pvf.Open(pathA)
if err != nil {
t.Fatal(err)
}
if err := core.setArchive(openA); err != nil {
t.Fatal(err)
}
defer core.closeArchive()
diffService := NewArchiveDiffService(core)
status, err := diffService.Start(ArchiveDiffRequest{Path: pathB})
if err != nil {
t.Fatal(err)
}
for i := 0; i < 50; i++ {
s, _ := diffService.Status(status.ID)
if s.State == "ready" {
break
}
time.Sleep(20 * time.Millisecond)
}
// 局部保存:只合入 [name],但 [price] 仍然不同
partialContent := "[name]\n`val_B`\n[price]\n`100`\n"
if err := diffService.MergeText(status.ID, "etc/test.etc", partialContent); err != nil {
t.Fatal(err)
}
// 验证局部保存后,etc/test.etc 仍然保留在差异列表中(因为 price 依然有差异)
flat, err := diffService.FlatFiles(status.ID, "", "all")
if err != nil {
t.Fatal(err)
}
if len(flat) != 1 || flat[0].Path != "etc/test.etc" {
t.Fatalf("expected etc/test.etc to remain in diff list, got %+v", flat)
}
// 完全保存:内容完全与 B 一致
fullContent := "[name]\n`val_B`\n[price]\n`200`\n"
if err := diffService.MergeText(status.ID, "etc/test.etc", fullContent); err != nil {
t.Fatal(err)
}
// 验证完全一致后,etc/test.etc 从差异列表中移除
flatAfter, err := diffService.FlatFiles(status.ID, "", "all")
if err != nil {
t.Fatal(err)
}
if len(flatAfter) != 0 {
t.Fatalf("expected etc/test.etc to be removed from diff list, got %+v", flatAfter)
}
}
func TestArchiveDiffMergeOneFilePreservesOtherDrafts(t *testing.T) {
dir := t.TempDir()
pathA := filepath.Join(dir, "base.pvf")
pathB := filepath.Join(dir, "other.pvf")
a := pvf.New()
if _, err := a.AddFileText("etc/file1.etc", "[name]\n`file1_orig`\n", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if _, err := a.AddFileText("etc/file2.etc", "[name]\n`file2_orig`\n", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if err := a.SaveAs(pathA); err != nil {
t.Fatal(err)
}
b := pvf.New()
if _, err := b.AddFileText("etc/file1.etc", "[name]\n`file1_other`\n", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if _, err := b.AddFileText("etc/file2.etc", "[name]\n`file2_other`\n", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if err := b.SaveAs(pathB); err != nil {
t.Fatal(err)
}
core := NewCore()
openA, err := pvf.Open(pathA)
if err != nil {
t.Fatal(err)
}
if err := core.setArchive(openA); err != nil {
t.Fatal(err)
}
defer core.closeArchive()
diffService := NewArchiveDiffService(core)
f1Idx, _ := core.archive.Find("etc/file1.etc")
f2Idx, _ := core.archive.Find("etc/file2.etc")
// 传入 file1 和 file2 的编辑器草稿
status, err := diffService.Start(ArchiveDiffRequest{
Path: pathB,
Drafts: []ArchiveDiffDraft{
{FileIndex: f1Idx, Path: "etc/file1.etc", Text: "[name]\n`file1_draft`\n"},
{FileIndex: f2Idx, Path: "etc/file2.etc", Text: "[name]\n`file2_draft`\n"},
},
})
if err != nil {
t.Fatal(err)
}
for i := 0; i < 50; i++ {
s, _ := diffService.Status(status.ID)
if s.State == "ready" {
break
}
time.Sleep(20 * time.Millisecond)
}
// 合并 file1
_, err = diffService.MergeFiles(status.ID, []string{"etc/file1.etc"})
if err != nil {
t.Fatal(err)
}
// 获取 file2 的详情,验证 file2 的对比底稿仍为 file2_draft,而不是回退成磁盘的 file2_orig
detail2, err := diffService.FileDiff(status.ID, "etc/file2.etc")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(detail2.AfterText, "file2_draft") {
t.Fatalf("expected file2 AfterText to preserve draft file2_draft, got: %q", detail2.AfterText)
}
}