DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

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

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

No files matched your search

+904
View File
@@ -0,0 +1,904 @@
package services
import (
"bytes"
"context"
"fmt"
"os"
"path/filepath"
"reflect"
"runtime"
"sort"
"strings"
"sync"
"testing"
"time"
"pvfine/internal/pvf"
pvfversion "pvfine/internal/version"
)
func addDiffText(t *testing.T, a *pvf.Archive, path, text string) int32 {
t.Helper()
i, err := a.AddFileText(path, text, pvf.TypeScript)
if err != nil {
t.Fatal(err)
}
return i
}
func waitDiff(t *testing.T, s *ArchiveDiffService, id string) *ArchiveDiffStatus {
t.Helper()
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
status, err := s.Status(id)
if err != nil {
t.Fatal(err)
}
if status.State == "ready" || status.State == "error" || status.State == "cancelled" {
return status
}
time.Sleep(time.Millisecond)
}
t.Fatal("comparison timed out")
return nil
}
func diffFixture(t *testing.T) (*core, *ArchiveDiffService, string) {
t.Helper()
other := pvf.New()
addDiffText(t, other, "equipment/same.equ", "[name]\n`same`")
addDiffText(t, other, "equipment/changed.equ", "[name]\n`old`")
addDiffText(t, other, "equipment/deleted.equ", "[name]\n`deleted`")
path := filepath.Join(t.TempDir(), "other.pvf")
if err := other.SaveAs(path); err != nil {
t.Fatal(err)
}
c := NewCore()
if err := c.setArchive(mustReopen(t, path)); err != nil {
t.Fatal(err)
}
s := NewArchiveDiffService(c)
t.Cleanup(c.closeArchive)
return c, s, path
}
func TestArchiveDiffWorkspaceSnapshot(t *testing.T) {
c, s, path := diffFixture(t)
a := c.archive
changed, _ := a.Find("equipment/changed.equ")
deleted, _ := a.Find("equipment/deleted.equ")
if err := a.SetText(changed, "[name]\n`backend edit`"); err != nil {
t.Fatal(err)
}
if _, err := a.RemoveFiles([]int32{deleted}); err != nil {
t.Fatal(err)
}
addDiffText(t, a, "equipment/added.equ", "[name]\n`added`")
changed, _ = a.Find("equipment/changed.equ")
original, err := a.CanonicalText(changed)
if err != nil {
t.Fatal(err)
}
checkpoint := a.MutationCheckpoint()
revision := c.batchRevision
indexGen := c.indexGen
generation := c.archiveGeneration
c.editorText[changed] = "retained editor overlay"
p, err := s.Start(ArchiveDiffRequest{Path: path, Drafts: []ArchiveDiffDraft{{FileIndex: changed, Path: "equipment/changed.equ", Text: "[name]\n`frontend draft`"}}})
if err != nil {
t.Fatal(err)
}
status := waitDiff(t, s, p.ID)
if status.State != "ready" || status.Added != 1 || status.Modified != 1 || status.Deleted != 1 {
t.Fatalf("status=%+v", status)
}
file, err := s.FileDiff(p.ID, "EQUIPMENT\\CHANGED.EQU")
if err != nil {
t.Fatal(err)
}
if !file.TextAvailable || !strings.Contains(file.BeforeText, "old") || !strings.Contains(file.AfterText, "frontend draft") {
t.Fatalf("diff=%+v", file)
}
if now, _ := a.CanonicalText(changed); now != original || c.archive != a || a.MutationCheckpoint() != checkpoint || c.batchRevision != revision || c.indexGen != indexGen || c.archiveGeneration != generation || c.editorText[changed] != "retained editor overlay" {
t.Fatal("comparison mutated workspace")
}
s.mu.Lock()
other := s.session.other
s.mu.Unlock()
if other.StringTableIndexStats().MappingBuilds != 0 || other.StringTableIndexStats().TableBuilds != 0 {
t.Fatal("comparison built string tables")
}
roots, err := s.Children(p.ID, "")
if err != nil || len(roots) != 1 || roots[0].Count != 3 {
t.Fatalf("roots=%+v error=%v", roots, err)
}
children, err := s.Children(p.ID, "EQUIPMENT")
if err != nil || len(children) != 3 {
t.Fatalf("children=%+v error=%v", children, err)
}
// Existing results are immutable; refresh reads the new backend payload.
if err := a.SetText(changed, "[name]\n`refreshed`"); err != nil {
t.Fatal(err)
}
file, _ = s.FileDiff(p.ID, "equipment/changed.equ")
if !strings.Contains(file.AfterText, "frontend draft") {
t.Fatal("snapshot changed")
}
fresh, err := s.Start(ArchiveDiffRequest{Path: path})
if err != nil {
t.Fatal(err)
}
if waitDiff(t, s, fresh.ID).State != "ready" {
t.Fatal("refresh failed")
}
if _, err := s.Status(p.ID); err == nil {
t.Fatal("old session retained")
}
file, _ = s.FileDiff(fresh.ID, "equipment/changed.equ")
if !strings.Contains(file.AfterText, "refreshed") {
t.Fatal("refresh missed latest content")
}
}
func TestArchiveDiffLogicalComparison(t *testing.T) {
old, new := pvf.New(), pvf.New()
addDiffText(t, old, "Item.equ", "[name]\n`equal`")
addDiffText(t, new, "unrelated.equ", "[name]\n`different pool layout`")
i := addDiffText(t, new, "item.equ", "[name]\n`equal`")
if _, err := new.RemoveFiles([]int32{0}); err != nil {
t.Fatal(err)
}
_ = i
before, err := archiveDiffSnapshot(context.Background(), old, 0, func(int) {})
if err != nil {
t.Fatal(err)
}
after, err := archiveDiffSnapshot(context.Background(), new, 0, func(int) {})
if err != nil {
t.Fatal(err)
}
if changes := pvfversion.Diff(before, after); len(changes) != 0 {
t.Fatalf("false changes=%+v", changes)
}
old.AddFile("empty.bin", nil, 99)
new.AddFile("empty.bin", nil, 98)
before, _ = archiveDiffSnapshot(context.Background(), old, 0, func(int) {})
after, _ = archiveDiffSnapshot(context.Background(), new, 0, func(int) {})
changes := pvfversion.Diff(before, after)
if len(changes) != 1 || changes[0].Operation != pvfversion.OperationModify {
t.Fatalf("type change=%+v", changes)
}
}
func TestArchiveDiffErrorsAndRelease(t *testing.T) {
c, s, path := diffFixture(t)
bad := filepath.Join(t.TempDir(), "bad.pvf")
if err := os.WriteFile(bad, []byte("invalid"), 0600); err != nil {
t.Fatal(err)
}
for _, request := range []ArchiveDiffRequest{{Path: bad}, {Path: path, Drafts: []ArchiveDiffDraft{{FileIndex: 999, Path: "missing", Text: "invalid"}}}} {
p, err := s.Start(request)
if err != nil {
t.Fatal(err)
}
if status := waitDiff(t, s, p.ID); status.State != "error" || status.Error == "" {
t.Fatalf("status=%+v", status)
}
}
p, err := s.Start(ArchiveDiffRequest{Path: path})
if err != nil {
t.Fatal(err)
}
if status := waitDiff(t, s, p.ID); status.State != "ready" || status.Added+status.Modified+status.Deleted != 0 {
t.Fatalf("equal status=%+v", status)
}
s.Cancel(p.ID)
status, _ := s.Status(p.ID)
if status.State != "cancelled" {
t.Fatal("not cancelled")
}
s.mu.Lock()
retained := s.session.current != nil || s.session.other != nil || s.session.children != nil
s.mu.Unlock()
if retained {
t.Fatal("cancel retains archives")
}
fresh, _ := s.Start(ArchiveDiffRequest{Path: path})
waitDiff(t, s, fresh.ID)
c.closeArchive()
if _, err := s.Status(fresh.ID); err == nil {
t.Fatal("workspace close retains session")
}
}
func TestArchiveDiffRejectsDuplicatePathsAndCancellation(t *testing.T) {
a := pvf.New()
a.AddFile("A.bin", []byte{1}, 99)
a.AddFile("b.bin", []byte{2}, 99)
var packed bytes.Buffer
if err := a.SaveTo(&packed); err != nil {
t.Fatal(err)
}
data := packed.Bytes()
// File-table records are plaintext: make the second entry reference the first path.
copy(data[0x30+0x18:0x30+0x18+8], data[0x30:0x30+8])
var parseErr error
a, parseErr = pvf.Parse(data)
if parseErr != nil {
t.Fatal(parseErr)
}
if _, err := archiveDiffSnapshot(context.Background(), a, 0, func(int) {}); err == nil || !strings.Contains(err.Error(), "重复路径") {
t.Fatalf("duplicate error=%v", err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if _, err := archiveDiffSnapshot(ctx, a, 0, func(int) {}); err != context.Canceled {
t.Fatalf("cancel error=%v", err)
}
}
func TestArchiveDiffBinaryEmptyAndLargeDetails(t *testing.T) {
c, s, path := diffFixture(t)
c.archive.AddFile("custom.bin", []byte{1, 2, 3}, 99)
addDiffText(t, c.archive, "empty.equ", "")
// Unicode data stays cheap to synthesize and exceeds the display threshold.
c.archive.AddFile("large.str", make([]byte, int(c.editableByteLimit())+2), pvf.TypeUnicode)
p, err := s.Start(ArchiveDiffRequest{Path: path})
if err != nil {
t.Fatal(err)
}
if status := waitDiff(t, s, p.ID); status.State != "ready" || status.Added != 3 {
t.Fatalf("status=%+v", status)
}
binary, err := s.FileDiff(p.ID, "custom.bin")
if err != nil {
t.Fatal(err)
}
if binary.TextAvailable || binary.Message == "" || binary.BeforeExists || !binary.AfterExists || binary.AfterSize != 3 {
t.Fatalf("binary=%+v", binary)
}
empty, err := s.FileDiff(p.ID, "empty.equ")
if err != nil {
t.Fatal(err)
}
if !empty.TextAvailable || empty.AfterText != "" || empty.Operation != "add" {
t.Fatalf("empty=%+v", empty)
}
large, err := s.FileDiff(p.ID, "large.str")
if err != nil {
t.Fatal(err)
}
if large.TextAvailable || !strings.Contains(large.Message, "阈值") {
t.Fatalf("large=%+v", large)
}
}
func TestArchiveDiffConcurrentRestartAndCancel(t *testing.T) {
_, s, path := diffFixture(t)
var workers sync.WaitGroup
errors := make(chan error, 12)
for worker := 0; worker < 3; worker++ {
workers.Add(1)
go func() {
defer workers.Done()
for attempt := 0; attempt < 4; attempt++ {
p, err := s.Start(ArchiveDiffRequest{Path: path})
if err != nil {
errors <- err
continue
}
_, _ = s.Status(p.ID)
_, _ = s.Children(p.ID, "")
_, _ = s.FileDiff(p.ID, "equipment/changed.equ")
s.Cancel(p.ID)
}
}()
}
workers.Wait()
close(errors)
for err := range errors {
t.Fatal(err)
}
final, err := s.Start(ArchiveDiffRequest{Path: path})
if err != nil {
t.Fatal(err)
}
if status := waitDiff(t, s, final.ID); status.State != "ready" {
t.Fatalf("final=%+v", status)
}
s.Release()
if _, err := s.Status(final.ID); err == nil {
t.Fatal("release retains final session")
}
}
func TestArchiveDiffHashMatchesVersionSnapshot(t *testing.T) {
a := pvf.New()
addDiffText(t, a, "item.equ", "[name]\n`logical`")
if _, err := a.AddFileText("text.str", "文本\r\n", pvf.TypeUnicode); err != nil {
t.Fatal(err)
}
a.AddFile("binary.bin", []byte{0, 1, 2, 255}, 99)
expected, err := pvfversion.SnapshotFromArchive(a)
if err != nil {
t.Fatal(err)
}
actual, err := archiveDiffSnapshot(context.Background(), a, 0, func(int) {})
if err != nil {
t.Fatal(err)
}
for path, entry := range expected {
if actual[path] != entry {
t.Fatalf("incompatible logical hash for %s", path)
}
}
}
func TestArchiveDiffParallelSnapshotMatchesSerialAndProgress(t *testing.T) {
a := pvf.New()
for i := 0; i < 256; i++ {
addDiffText(t, a, fmt.Sprintf("scripts/%04d.equ", i), fmt.Sprintf("[name]\n`item %d`\n[price]\n%d", i, i*10))
a.AddFile(fmt.Sprintf("binary/%04d.bin", i), bytes.Repeat([]byte{byte(i)}, 1024), 99)
}
expected, err := pvfversion.SnapshotFromArchive(a)
if err != nil {
t.Fatal(err)
}
for _, workers := range []int{1, 4, 13, 32} {
t.Run(fmt.Sprint(workers), func(t *testing.T) {
progress := 37
actual, err := archiveDiffSnapshotWorkers(context.Background(), a, 37, func(n int) {
if n < progress || n > 37+int(a.FileCount()) {
t.Errorf("invalid progress %d after %d", n, progress)
}
progress = n
}, workers)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(actual, expected) {
t.Fatal("parallel snapshot differs from serial logical snapshot")
}
if progress != 37+int(a.FileCount()) {
t.Fatalf("incomplete progress %d", progress)
}
})
}
if got := archiveDiffWorkerCount(a.FileCount()); got != min(int(a.FileCount()), max(1, runtime.GOMAXPROCS(0)-1)) {
t.Fatalf("worker count %d does not follow available CPUs", got)
}
if archiveDiffWorkerCount(1) != 1 || archiveDiffWorkerCount(0) != 0 {
t.Fatal("worker count exceeds file count")
}
}
func TestArchiveDiffParallelCancellationDrainsWorkers(t *testing.T) {
a := pvf.New()
for i := 0; i < 512; i++ {
a.AddFile(fmt.Sprintf("%04d.bin", i), bytes.Repeat([]byte{byte(i)}, 4096), 99)
}
ctx, cancel := context.WithCancel(context.Background())
last := 0
_, err := archiveDiffSnapshotWorkers(ctx, a, 0, func(n int) { last = n; cancel() }, 13)
if err != context.Canceled {
t.Fatalf("cancellation error=%v", err)
}
if last != 1 {
t.Fatalf("progress continued after cancellation: %d", last)
}
// A fresh scan must remain usable after all cancelled workers exit.
result, err := archiveDiffSnapshotWorkers(context.Background(), a, 0, func(int) {}, 13)
if err != nil || len(result) != int(a.FileCount()) {
t.Fatalf("fresh scan=%d error=%v", len(result), err)
}
}
func BenchmarkArchiveDiffParallelSnapshot(b *testing.B) {
for _, kind := range []string{"script", "binary"} {
a := pvf.New()
for i := 0; i < 1024; i++ {
path := fmt.Sprintf("files/%04d", i)
if kind == "script" {
if _, err := a.AddFileText(path+".equ", "[values]\n"+strings.Repeat("1 2 3 4 5 6 7 8\n", 128), pvf.TypeScript); err != nil {
b.Fatal(err)
}
} else {
a.AddFile(path+".bin", bytes.Repeat([]byte{byte(i)}, 64<<10), 99)
}
}
for _, workers := range []int{1, 4, 13} {
b.Run(fmt.Sprintf("%s/workers-%d", kind, workers), func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if _, err := archiveDiffSnapshotWorkers(context.Background(), a, 0, func(int) {}, workers); err != nil {
b.Fatal(err)
}
}
})
}
}
}
// Previous text-hash implementation is kept only as a correctness/performance oracle.
func archiveDiffSnapshot(ctx context.Context, a *pvf.Archive, offset int, progress func(int)) (pvfversion.Snapshot, error) {
return archiveDiffSnapshotWorkers(ctx, a, offset, progress, archiveDiffWorkerCount(a.FileCount()))
}
type archiveDiffHashResult struct {
key string
entry pvfversion.Entry
err error
}
func archiveDiffSnapshotWorkers(parent context.Context, a *pvf.Archive, offset int, progress func(int), workers int) (pvfversion.Snapshot, error) {
if err := parent.Err(); err != nil {
return nil, err
}
result := make(pvfversion.Snapshot, a.FileCount())
if a.FileCount() == 0 {
progress(offset)
return result, nil
}
workers = min(int(a.FileCount()), max(1, workers))
ctx, cancel := context.WithCancel(parent)
defer cancel()
// Queue indexes rather than decoded payloads. Each worker retains only the
// file it is hashing; the bounded result queue contains compact hashes.
jobs := make(chan int32)
results := make(chan archiveDiffHashResult, workers)
largeFile := make(chan struct{}, 1)
var done sync.WaitGroup
done.Add(workers + 1)
go func() {
defer done.Done()
defer close(jobs)
for i := int32(0); i < a.FileCount(); i++ {
select {
case jobs <- i:
case <-ctx.Done():
return
}
}
}()
for worker := 0; worker < workers; worker++ {
go func() {
defer done.Done()
for {
var index int32
var ok bool
select {
case <-ctx.Done():
return
case index, ok = <-jobs:
if !ok {
return
}
}
file := a.File(index)
// Large logical text expansion is the main transient memory risk. Allow
// at most one >=8 MiB payload to decode/hash at a time, independently of
// the CPU-based worker count used for ordinary files.
large := file.DataSize >= 8<<20
if large {
select {
case largeFile <- struct{}{}:
case <-ctx.Done():
return
}
}
value := archiveDiffHashFile(ctx, a, index, file)
if large {
<-largeFile
}
select {
case results <- value:
case <-ctx.Done():
return
}
if value.err != nil {
return
}
}
}()
}
go func() { done.Wait(); close(results) }()
processed := 0
var firstErr error
// One collector owns the map and progress updates, keeping progress ordered
// even when file computations finish out of order. Drain after cancellation
// so no producer or worker survives a released comparison task.
for value := range results {
if firstErr != nil || ctx.Err() != nil {
continue
}
if value.err != nil {
firstErr = value.err
} else if _, exists := result[value.key]; exists {
firstErr = fmt.Errorf("归档包含重复路径 %q,无法唯一比较", value.entry.Path)
}
if firstErr != nil {
cancel()
continue
}
result[value.key] = value.entry
processed++
if processed == 1 || processed%64 == 0 {
progress(offset + processed)
}
}
if firstErr != nil {
return nil, firstErr
}
if err := parent.Err(); err != nil {
return nil, err
}
progress(offset + processed)
return result, nil
}
func archiveDiffHashFile(ctx context.Context, a *pvf.Archive, index int32, file pvf.File) archiveDiffHashResult {
if err := ctx.Err(); err != nil {
return archiveDiffHashResult{err: err}
}
path := pvfversion.DisplayPath(a.Path(index))
key := pvfversion.CanonicalPath(path)
if key == "" {
return archiveDiffHashResult{err: fmt.Errorf("归档中存在空路径")}
}
var data []byte
var err error
if file.DataType == pvf.TypeScript || file.DataType == pvf.TypeUnicode {
var value string
value, err = a.CanonicalText(index)
data = []byte(value)
} else {
data, err = a.RawBytes(index)
}
if err != nil {
return archiveDiffHashResult{err: fmt.Errorf("读取 %s 失败: %w", path, err)}
}
if err := ctx.Err(); err != nil {
return archiveDiffHashResult{err: err}
}
return archiveDiffHashResult{key: key, entry: pvfversion.Entry{Path: path, DataType: file.DataType, Hash: archiveDiffContentHash(file.DataType, data)}}
}
func TestArchiveDiffOptimizedComparisonMatchesCanonicalOracle(t *testing.T) {
before, after := pvf.New(), pvf.New()
addDiffText(t, after, "temporary.equ", "[name]\n`different pool offsets`")
for i := 0; i < 256; i++ {
path := fmt.Sprintf("equipment/%03d.equ", i)
text := fmt.Sprintf("[name]\n`item %d`\n[price]\n%d", i, i*10)
addDiffText(t, before, path, text)
addDiffText(t, after, path, text)
}
if _, err := after.RemoveFiles([]int32{0}); err != nil {
t.Fatal(err)
}
index, _ := after.Find("equipment/010.equ")
if err := after.SetText(index, "[name]\n`changed`\n[price]\n42"); err != nil {
t.Fatal(err)
}
index, _ = after.Find("equipment/020.equ")
if _, err := after.RemoveFiles([]int32{index}); err != nil {
t.Fatal(err)
}
addDiffText(t, after, "equipment/new.equ", "[name]\n`added`")
before.AddFile("fallback.equ", []byte{1, 1, 0, 0, 0}, pvf.TypeScript)
after.AddFile("fallback.equ", []byte{9, 2, 0, 0, 0}, pvf.TypeScript) // Both ignored by canonical rendering.
before.AddFile("trailing.equ", []byte{0, 1, 0, 0, 0}, pvf.TypeScript)
after.AddFile("trailing.equ", []byte{0, 1, 0, 0, 0, 255}, pvf.TypeScript)
before.AddFile("text.str", []byte{'A', 0}, pvf.TypeUnicode)
after.AddFile("text.str", []byte{'A', 0, 255}, pvf.TypeUnicode) // UTF-16 decoder ignores the odd tail.
before.AddFile("nan.equ", []byte{2, 1, 0, 192, 127}, pvf.TypeScript)
after.AddFile("nan.equ", []byte{2, 2, 0, 192, 127}, pvf.TypeScript)
before.AddFile("type.bin", []byte{1}, 99)
after.AddFile("type.bin", []byte{1}, 100)
expectedBefore, err := pvfversion.SnapshotFromArchive(before)
if err != nil {
t.Fatal(err)
}
expectedAfter, err := pvfversion.SnapshotFromArchive(after)
if err != nil {
t.Fatal(err)
}
expected := pvfversion.Diff(expectedBefore, expectedAfter)
previous := 0
actual, err := archiveDiffCompare(context.Background(), before, after, func(n int) {
if n < previous {
t.Errorf("progress regressed: %d -> %d", previous, n)
}
previous = n
})
if err != nil {
t.Fatal(err)
}
if len(actual) != len(expected) {
t.Fatalf("optimized changes=%+v expected=%+v", actual, expected)
}
for i := range actual {
if actual[i].Path != expected[i].Path || actual[i].Operation != expected[i].Operation {
t.Fatalf("change=%+v expected=%+v", actual[i], expected[i])
}
if actual[i].BeforeHash != "" && actual[i].BeforeHash != expected[i].BeforeHash {
t.Fatal("before hash no longer canonical")
}
if actual[i].AfterHash != "" && actual[i].AfterHash != expected[i].AfterHash {
t.Fatal("after hash no longer canonical")
}
}
if previous != int(before.FileCount()+after.FileCount()) {
t.Fatal("progress did not finish")
}
}
func TestArchiveDiffSkipsUnpairedPayloadAndHashesDetailsOnDemand(t *testing.T) {
c, s, path := diffFixture(t)
added := addDiffText(t, c.archive, "added.equ", "[price]\n42")
p, err := s.Start(ArchiveDiffRequest{Path: path})
if err != nil {
t.Fatal(err)
}
if status := waitDiff(t, s, p.ID); status.State != "ready" {
t.Fatalf("status=%+v", status)
}
s.mu.Lock()
change := s.session.changes["added.equ"]
s.mu.Unlock()
if change.AfterHash != "" {
t.Fatal("added file was unnecessarily hashed during classification")
}
meta, err := s.FileDiff(p.ID, "added.equ")
if err != nil {
t.Fatal(err)
}
text, err := c.archive.CanonicalText(added)
if err != nil {
t.Fatal(err)
}
if !meta.TextAvailable || meta.AfterHash != archiveDiffContentHash(pvf.TypeScript, []byte(text)) {
t.Fatalf("lazy detail=%+v", meta)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if _, err := archiveDiffCompare(ctx, c.archive, c.archive, func(int) {}); err != context.Canceled {
t.Fatalf("cancel=%v", err)
}
}
func BenchmarkArchiveDiffOptimizedComparison(b *testing.B) {
a := pvf.New()
for i := 0; i < 2048; i++ {
if _, err := a.AddFileText(fmt.Sprintf("items/%04d.equ", i), "[name]\n`common value`\n[values]\n"+strings.Repeat("1 2 3 4 5 6 7 8\n", 64), pvf.TypeScript); err != nil {
b.Fatal(err)
}
}
var packed bytes.Buffer
if err := a.SaveTo(&packed); err != nil {
b.Fatal(err)
}
before, err := pvf.Parse(packed.Bytes())
if err != nil {
b.Fatal(err)
}
after, err := pvf.Parse(packed.Bytes())
if err != nil {
b.Fatal(err)
}
for _, kind := range []string{"canonical-text", "token-chunks"} {
b.Run(kind, func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if kind == "canonical-text" {
old, err := archiveDiffSnapshot(context.Background(), before, 0, func(int) {})
if err != nil {
b.Fatal(err)
}
next, err := archiveDiffSnapshot(context.Background(), after, 0, func(int) {})
if err != nil {
b.Fatal(err)
}
if len(pvfversion.Diff(old, next)) != 0 {
b.Fatal("fixture differs")
}
} else {
changes, err := archiveDiffCompare(context.Background(), before, after, func(int) {})
if err != nil {
b.Fatal(err)
}
if len(changes) != 0 {
b.Fatal("fixture differs")
}
}
}
})
}
}
func BenchmarkArchiveDiffOversizedColdChunk(b *testing.B) {
a := pvf.New()
a.AddFile("padding.bin", make([]byte, (64<<20)+1), 99)
for i := 0; i < 8; i++ {
if _, err := a.AddFileText(fmt.Sprintf("items/%02d.equ", i), "[name]\n`shared value`\n[price]\n42", pvf.TypeScript); err != nil {
b.Fatal(err)
}
}
var packed bytes.Buffer
if err := a.SaveTo(&packed); err != nil {
b.Fatal(err)
}
data := packed.Bytes()
for _, kind := range []string{"canonical-text", "token-chunks"} {
b.Run(kind, func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
before, err := pvf.Parse(data)
if err != nil {
b.Fatal(err)
}
after, err := pvf.Parse(data)
if err != nil {
b.Fatal(err)
}
if kind == "canonical-text" {
old, err := archiveDiffSnapshot(context.Background(), before, 0, func(int) {})
if err != nil {
b.Fatal(err)
}
next, err := archiveDiffSnapshot(context.Background(), after, 0, func(int) {})
if err != nil {
b.Fatal(err)
}
if len(pvfversion.Diff(old, next)) != 0 {
b.Fatal("fixture differs")
}
} else {
changes, err := archiveDiffCompare(context.Background(), before, after, func(int) {})
if err != nil {
b.Fatal(err)
}
if len(changes) != 0 {
b.Fatal("fixture differs")
}
}
}
})
}
}
func TestArchiveDiffDetailCacheIsIsolatedAndReleased(t *testing.T) {
c, s, path := diffFixture(t)
i, _ := c.archive.Find("equipment/changed.equ")
if err := c.archive.SetText(i, "[name]\n`cached`"); err != nil {
t.Fatal(err)
}
p, err := s.Start(ArchiveDiffRequest{Path: path})
if err != nil {
t.Fatal(err)
}
if status := waitDiff(t, s, p.ID); status.State != "ready" {
t.Fatalf("status=%+v", status)
}
first, err := s.FileDiff(p.ID, "equipment/changed.equ")
if err != nil {
t.Fatal(err)
}
want := *first
first.AfterText = "caller mutation"
again, err := s.FileDiff(p.ID, "EQUIPMENT\\CHANGED.EQU")
if err != nil || *again != want {
t.Fatalf("cached value was mutated: %v", err)
}
s.mu.Lock()
cached := s.session.detail
s.mu.Unlock()
if cached == nil {
t.Fatal("small detail was not cached")
}
s.Release()
if _, err := s.FileDiff(p.ID, "equipment/changed.equ"); err == nil {
t.Fatal("released detail remained accessible")
}
}
func archiveDiffLegacyChildren(changes []pvfversion.FileChange) map[string][]ArchiveDiffNode {
nodes := make(map[string]map[string]ArchiveDiffNode)
for _, change := range changes {
parts := strings.Split(change.DisplayPath, "/")
parent := ""
for i, name := range parts {
path := strings.Join(parts[:i+1], "/")
key := pvfversion.CanonicalPath(path)
if nodes[parent] == nil {
nodes[parent] = make(map[string]ArchiveDiffNode)
}
node := nodes[parent][key]
node.Path = path
node.Name = name
node.Count++
node.IsDir = i < len(parts)-1
if !node.IsDir {
node.Operation = string(change.Operation)
}
nodes[parent][key] = node
parent = key
}
}
result := make(map[string][]ArchiveDiffNode, len(nodes))
for parent, entries := range nodes {
list := make([]ArchiveDiffNode, 0, len(entries))
for _, node := range entries {
list = append(list, node)
}
sort.Slice(list, func(i, j int) bool {
if list[i].IsDir != list[j].IsDir {
return list[i].IsDir
}
return strings.ToLower(list[i].Name) < strings.ToLower(list[j].Name)
})
result[parent] = list
}
return result
}
func diffTreeChanges(n int) []pvfversion.FileChange {
changes := make([]pvfversion.FileChange, n)
for i := range changes {
path := fmt.Sprintf("equipment/group%03d/subdir/item%06d.equ", i%100, i)
changes[i] = pvfversion.FileChange{Path: path, DisplayPath: path, Operation: pvfversion.OperationModify}
}
return changes
}
func TestArchiveDiffTreeMatchesPreviousAndCancels(t *testing.T) {
changes := diffTreeChanges(4096)
got := archiveDiffChildren(changes)
if !reflect.DeepEqual(got, archiveDiffLegacyChildren(changes)) {
t.Fatal("tree paths, order or directory counts changed")
}
if len(archiveDiffChildren(nil)) != 0 {
t.Fatal("empty comparison has tree nodes")
}
ctx, cancel := context.WithCancel(context.Background())
_, err := archiveDiffBuildChildren(ctx, changes, func(state string, n, total int) {
if state == "building" && n >= 64 {
cancel()
}
})
if err != context.Canceled {
t.Fatalf("tree cancellation=%v", err)
}
}
func TestArchiveDiffReportsTextVerificationPhase(t *testing.T) {
before, after := pvf.New(), pvf.New()
addDiffText(t, before, "changed.equ", "[name]\n`old`")
addDiffText(t, after, "changed.equ", "[name]\n`new`")
var phases []string
last := map[string]int{}
changes, err := archiveDiffComparePhases(context.Background(), before, after, func(int) {}, func(state string, n, total int) {
if n < last[state] || n > total {
t.Errorf("invalid phase progress %s: %d/%d", state, n, total)
}
last[state] = n
phases = append(phases, state)
})
if err != nil || len(changes) != 1 || last["verifying"] != 2 {
t.Fatalf("missing text verification progress: %v %v", phases, err)
}
}
func BenchmarkArchiveDiffTree(b *testing.B) {
changes := diffTreeChanges(100000)
for _, kind := range []string{"previous", "directory-cache"} {
b.Run(kind, func(b *testing.B) {
b.ReportAllocs()
for range b.N {
var nodes map[string][]ArchiveDiffNode
if kind == "previous" {
nodes = archiveDiffLegacyChildren(changes)
} else {
nodes = archiveDiffChildren(changes)
}
if len(nodes["equipment"]) != 100 {
b.Fatal("incorrect tree")
}
}
})
}
}