Files
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

285 lines
9.4 KiB
Go

package services
import (
"bytes"
"errors"
"fmt"
"log"
"strings"
"sync"
"testing"
"time"
"pvfine/internal/buildmode"
)
func TestDebugTimingLogsUseDebugLevel(t *testing.T) {
before := applicationLogs.snapshot()
lastID := uint64(0)
if len(before) > 0 {
lastID = before[len(before)-1].ID
}
finish := debugPhase("test-phase", "file=42")
debugLockAcquired("test-phase", time.Now())
finish()
var entries []appLogEntry
for _, entry := range applicationLogs.snapshot() {
if entry.ID > lastID {
entries = append(entries, entry)
}
}
if !buildmode.Development {
if len(entries) != 0 {
t.Fatalf("production emitted performance logs: %#v", entries)
}
return
}
if len(entries) != 3 {
t.Fatalf("expected start, lock wait and finish logs, got %d", len(entries))
}
for _, entry := range entries {
if entry.Level != "DEBUG" || entry.Source != "performance" {
t.Fatalf("unexpected entry: %#v", entry)
}
}
for i, expected := range []string{"started file=42", "core-lock acquired wait=", "finished file=42 elapsed="} {
if !strings.Contains(entries[i].Message, expected) {
t.Fatalf("entry %d: %s", i, entries[i].Message)
}
}
}
func TestLogBufferBoundedAndSnapshotIndependent(t *testing.T) {
var buffer logBuffer
for i := 0; i < logCapacity+10; i++ {
buffer.append("INFO", "test", fmt.Sprint(i))
}
snapshot := buffer.snapshot()
if len(snapshot) != logCapacity || snapshot[0].ID != 11 || snapshot[len(snapshot)-1].ID != logCapacity+10 {
t.Fatalf("unexpected snapshot bounds: %d, %d, %d", len(snapshot), snapshot[0].ID, snapshot[len(snapshot)-1].ID)
}
snapshot[0].Message = "changed"
if buffer.snapshot()[0].Message == "changed" {
t.Fatal("snapshot aliases the buffer")
}
}
func TestLogBufferConcurrent(t *testing.T) {
var buffer logBuffer
var workers sync.WaitGroup
for i := 0; i < 8; i++ {
workers.Go(func() {
for j := 0; j < 100; j++ {
buffer.append("INFO", "test", "entry")
buffer.snapshot()
}
})
}
workers.Wait()
snapshot := buffer.snapshot()
if len(snapshot) != 800 {
t.Fatalf("entries = %d", len(snapshot))
}
for i, entry := range snapshot {
if entry.ID != uint64(i+1) {
t.Fatalf("non-sequential ID at %d: %d", i, entry.ID)
}
}
}
func TestClassifyLogLevel(t *testing.T) {
for _, test := range []struct{ message, level string }{
{"[ERR] broken", "ERROR"},
{"level=ERROR msg=broken", "ERROR"},
{"index failed: error=broken", "ERROR"},
{"打开失败", "ERROR"},
{"prepare finished elapsed=2ms error=<nil>", "INFO"},
{"level=WARN msg=warning", "WARN"},
{"cache publication unavailable", "WARN"},
{"level=DEBUG msg=trace", "DEBUG"},
{"index finished elapsed=100ms", "INFO"},
{"index finished skipped=0", "INFO"},
} {
if got := classifyLogLevel(test.message); got != test.level {
t.Errorf("%q: got %s, want %s", test.message, got, test.level)
}
}
}
func TestServiceErrorsAreLoggedWithoutChangingSerialization(t *testing.T) {
var output bytes.Buffer
original := log.Writer()
log.SetOutput(CaptureLogs(&output))
t.Cleanup(func() { log.SetOutput(original) })
if result := LogServiceError(errors.New("service failure")); result != nil {
t.Fatal("must use the default Wails serialization")
}
if !strings.Contains(output.String(), "[ERR] service failure") {
t.Fatalf("missing error output: %s", output.String())
}
snapshot := applicationLogs.snapshot()
last := snapshot[len(snapshot)-1]
if last.Level != "ERROR" || !strings.Contains(last.Message, "service failure") {
t.Fatalf("error entry = %#v", last)
}
if result := LogServiceError(nil); result != nil {
t.Fatal("nil errors must also use default serialization")
}
}
func TestCaptureLogsPreservesOutput(t *testing.T) {
var output bytes.Buffer
writer := CaptureLogs(&output)
line := "[ERR] 日志错误\n"
n, err := writer.Write([]byte(line))
if err != nil || n != len(line) || output.String() != line {
t.Fatalf("write = %d, %v, %q", n, err, output.String())
}
}
func TestDescribeArchiveAndIndexLogs(t *testing.T) {
level, message, _, _ := describeLogEvent("archive:opened", ArchiveInfo{
Path: "test.pvf", FileCount: 20, Format: "paged110", Paged110: true,
})
if level != "INFO" || !strings.Contains(message, "test.pvf") || !strings.Contains(message, "文件=20") || !strings.Contains(message, "变体=paged110") {
t.Fatalf("archive log = %s, %s", level, message)
}
first := IndexStatus{State: "building", Stage: "strings", Done: 1, Total: 100}
_, _, key, signature := describeLogEvent("archive:index-progress", first)
first.Done++
_, _, nextKey, nextSignature := describeLogEvent("archive:index-progress", first)
if key != nextKey || signature != nextSignature {
t.Fatal("per-file progress must not produce a new stage signature")
}
first.State, first.Stage, first.BuildDurationMs, first.OpenDurationMs = "ready", "ready-cache", 42, 12
_, message, _, nextSignature = describeLogEvent("archive:index-ready", first)
if signature == nextSignature || !strings.Contains(message, "42.00 ms") || !strings.Contains(message, "12.00 ms") {
t.Fatalf("ready log missing timing: %s", message)
}
first.RefreshError = "refresh failed"
level, message, _, _ = describeLogEvent("archive:index-ready", first)
if level != "ERROR" || !strings.Contains(message, "refresh failed") {
t.Fatalf("refresh error = %s, %s", level, message)
}
}
func TestDescribeOtherEvents(t *testing.T) {
for _, name := range []string{"archive:closed", "archive:file-index-ready", "archive:script-applied", "version:committed"} {
if _, message, _, _ := describeLogEvent(name, nil); message == "" {
t.Errorf("%s has no log", name)
}
}
level, message, _, _ := describeLogEvent("script:log", map[string]any{"level": "warn", "message": "脚本警告"})
if level != "WARN" || message != "脚本警告" {
t.Fatalf("script log = %s, %s", level, message)
}
level, message, _, _ = describeLogEvent("image:index-error", ImageIndexStatus{State: "error", Error: "broken", BuildDurationMs: 10})
if level != "ERROR" || !strings.Contains(message, "broken") || !strings.Contains(message, "10.00 ms") {
t.Fatalf("image log = %s, %s", level, message)
}
}
func TestLogForwardingWaitsForEventInitialization(t *testing.T) {
var buffer logBuffer
buffer.record("INFO", "Go", "Build Info:")
var forwarded []appLogEntry
buffer.setEmitter(func(entry appLogEntry) {
// Reading history here must not deadlock with the recording lock.
_ = buffer.snapshot()
forwarded = append(forwarded, entry)
})
buffer.record("ERROR", "Go", "service failure")
if len(forwarded) != 1 || forwarded[0].Message != "service failure" {
t.Fatalf("live logs = %#v", forwarded)
}
history := buffer.snapshot()
if !buildmode.Development {
if len(history) != 1 || history[0] != forwarded[0] {
t.Fatalf("production history = %#v", history)
}
return
}
if len(history) != 2 || history[0].Message != "Build Info:" || history[1] != forwarded[0] {
t.Fatalf("startup history = %#v", history)
}
}
func TestLogForwardingConcurrentInitialization(t *testing.T) {
var buffer logBuffer
var workers sync.WaitGroup
workers.Go(func() {
for range 100 {
buffer.setEmitter(func(appLogEntry) {})
}
})
workers.Go(func() {
for range 100 {
buffer.record("INFO", "application", "startup")
}
})
workers.Wait()
if got := len(buffer.snapshot()); got != 100 {
t.Fatalf("history entries = %d, want 100", got)
}
}
type logFormattingProbe struct{ formatted bool }
func (p *logFormattingProbe) String() string {
p.formatted = true
return "formatting-probe"
}
func TestDevelopmentLogsRespectBuildModeBeforeFormatting(t *testing.T) {
var output bytes.Buffer
original := log.Writer()
log.SetOutput(CaptureLogs(&output))
t.Cleanup(func() { log.SetOutput(original) })
probe := &logFormattingProbe{}
developmentLog("[DEBUG] %s", probe)
debugLog("%s", probe)
developmentOrErrorLog(nil, "success %s", probe)
if probe.formatted != buildmode.Development {
t.Fatalf("formatted=%t development=%t", probe.formatted, buildmode.Development)
}
if !buildmode.Development && output.Len() != 0 {
t.Fatalf("production wrote development logs: %s", output.String())
}
developmentOrErrorLog(errors.New("failed"), "operation error=%v", errors.New("failed"))
if !strings.Contains(output.String(), "operation error=failed") {
t.Fatal("operation errors must remain visible")
}
}
func TestCaptureLogsProductionPolicyPreservesErrorsAndContext(t *testing.T) {
var output bytes.Buffer
writer := CaptureLogs(&output)
for _, message := range []string{
"[DEBUG] runtime details\n", "runtime startup\n", "level=WARN runtime warning\n", "[ERR] runtime error\n exception context\n",
} {
if n, err := writer.Write([]byte(message)); err != nil || n != len(message) {
t.Fatalf("write = %d, %v", n, err)
}
}
text := output.String()
if strings.Contains(text, "runtime details") != buildmode.Development || strings.Contains(text, "runtime startup") != buildmode.Development {
t.Fatalf("runtime output policy mismatch: %s", text)
}
if !strings.Contains(text, "runtime warning") || !strings.Contains(text, "runtime error\n exception context") {
t.Fatalf("warning/error context lost: %s", text)
}
var buffer logBuffer
buffer.record("DEBUG", "performance", "trace")
buffer.record("INFO", "Go", "runtime startup")
buffer.record("INFO", "archive:opened", "opened")
buffer.record("WARN", "Go", "warning")
buffer.record("ERROR", "Go", "error")
want := 3
if buildmode.Development {
want = 5
}
if entries := buffer.snapshot(); len(entries) != want {
t.Fatalf("retained %d entries, want %d", len(entries), want)
}
}