服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
474 lines
19 KiB
Go
474 lines
19 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"longheng.io/server/internal/modules/dnf/channelcatalog"
|
|
"longheng.io/server/internal/modules/dnf/raid"
|
|
"longheng.io/server/internal/platform/timequeue"
|
|
)
|
|
|
|
func TestActivateRuntimeRaidPhaseBroadcastFailureAllowsSamePhaseRetry(t *testing.T) {
|
|
preparedAt := time.Date(2026, 8, 6, 4, 0, 0, 0, time.UTC)
|
|
wantWriteErr := errors.New("raid phase announcement write failed")
|
|
connection := &runtimeRaidPhaseTestConn{remainingFailures: 1, writeErr: wantWriteErr}
|
|
queue := &runtimeRaidPhaseTestQueue{}
|
|
service, raidKey, activationAt := newRuntimeRaidPhaseActivationTestService(t, preparedAt, connection, queue)
|
|
queue.now = activationAt
|
|
|
|
if _, err := service.activateRuntimeRaidPhase(raidKey, runtimeRaidDungeonPhaseOne, activationAt, false); !errors.Is(err, wantWriteErr) {
|
|
t.Fatalf("first activation error=%v want=%v", err, wantWriteErr)
|
|
}
|
|
if queue.scheduleCalls != 0 {
|
|
t.Fatalf("phase expiry scheduled after failed announcement: calls=%d", queue.scheduleCalls)
|
|
}
|
|
committed := cloneRuntimeRaidState(*service.raids[raidKey])
|
|
if committed.Lifecycle.Status != runtimeRaidLifecyclePhaseActive || committed.Lifecycle.Phase != runtimeRaidDungeonPhaseOne {
|
|
t.Fatalf("committed lifecycle=%+v", committed.Lifecycle)
|
|
}
|
|
if !committed.Lifecycle.PhaseStartedAt.Equal(activationAt) {
|
|
t.Fatalf("committed phase start=%s want=%s", committed.Lifecycle.PhaseStartedAt, activationAt)
|
|
}
|
|
|
|
retryAt := activationAt.Add(17*time.Second + 200*time.Millisecond)
|
|
queue.now = retryAt
|
|
retried, err := service.activateRuntimeRaidPhase(raidKey, runtimeRaidDungeonPhaseOne, retryAt, false)
|
|
if err != nil {
|
|
t.Fatalf("same-phase retry: %v", err)
|
|
}
|
|
if !retried.Lifecycle.PhaseStartedAt.Equal(committed.Lifecycle.PhaseStartedAt) ||
|
|
!retried.Lifecycle.PhaseDeadline.Equal(committed.Lifecycle.PhaseDeadline) ||
|
|
retried.Lifecycle.Revision != committed.Lifecycle.Revision {
|
|
t.Fatalf("retry reset phase lifecycle: before=%+v after=%+v", committed.Lifecycle, retried.Lifecycle)
|
|
}
|
|
wantRemaining := runtimeRaidDurationSeconds(committed.Lifecycle.PhaseDeadline.Sub(retryAt))
|
|
assertRuntimeRaidPhaseActivationPackets(t, connection.write.Bytes(), runtimeRaidDungeonPhaseOne, wantRemaining)
|
|
if queue.scheduleCalls != 1 || queue.scheduledName != "raid-phase:13108201:1:1" {
|
|
t.Fatalf("phase expiry reconstruction calls=%d name=%q", queue.scheduleCalls, queue.scheduledName)
|
|
}
|
|
if wantDelay := committed.Lifecycle.PhaseDeadline.Sub(retryAt); queue.scheduledDelay != wantDelay {
|
|
t.Fatalf("phase expiry delay=%s want=%s", queue.scheduledDelay, wantDelay)
|
|
}
|
|
}
|
|
|
|
func TestActivateRuntimeRaidPhaseSchedulerFailureAllowsTimerReconstruction(t *testing.T) {
|
|
preparedAt := time.Date(2026, 8, 6, 5, 0, 0, 0, time.UTC)
|
|
wantScheduleErr := errors.New("raid phase schedule failed")
|
|
connection := &runtimeRaidPhaseTestConn{}
|
|
queue := &runtimeRaidPhaseTestQueue{remainingScheduleFailures: 1, scheduleErr: wantScheduleErr}
|
|
service, raidKey, activationAt := newRuntimeRaidPhaseActivationTestService(t, preparedAt, connection, queue)
|
|
queue.now = activationAt
|
|
|
|
if _, err := service.activateRuntimeRaidPhase(raidKey, runtimeRaidDungeonPhaseOne, activationAt, false); !errors.Is(err, wantScheduleErr) {
|
|
t.Fatalf("first activation error=%v want=%v", err, wantScheduleErr)
|
|
}
|
|
committed := cloneRuntimeRaidState(*service.raids[raidKey])
|
|
assertRuntimeRaidPhaseActivationPackets(
|
|
t,
|
|
connection.write.Bytes(),
|
|
runtimeRaidDungeonPhaseOne,
|
|
runtimeRaidDurationSeconds(committed.Lifecycle.PhaseDeadline.Sub(activationAt)),
|
|
)
|
|
connection.write.Reset()
|
|
|
|
retryAt := activationAt.Add(9*time.Second + 500*time.Millisecond)
|
|
queue.now = retryAt
|
|
retried, err := service.activateRuntimeRaidPhase(raidKey, runtimeRaidDungeonPhaseOne, retryAt, false)
|
|
if err != nil {
|
|
t.Fatalf("same-phase timer reconstruction: %v", err)
|
|
}
|
|
if !retried.Lifecycle.PhaseStartedAt.Equal(committed.Lifecycle.PhaseStartedAt) ||
|
|
!retried.Lifecycle.PhaseDeadline.Equal(committed.Lifecycle.PhaseDeadline) ||
|
|
retried.Lifecycle.Revision != committed.Lifecycle.Revision {
|
|
t.Fatalf("timer reconstruction reset phase lifecycle: before=%+v after=%+v", committed.Lifecycle, retried.Lifecycle)
|
|
}
|
|
wantRemaining := runtimeRaidDurationSeconds(committed.Lifecycle.PhaseDeadline.Sub(retryAt))
|
|
assertRuntimeRaidPhaseActivationPackets(t, connection.write.Bytes(), runtimeRaidDungeonPhaseOne, wantRemaining)
|
|
if queue.scheduleCalls != 2 || queue.callback == nil {
|
|
t.Fatalf("phase expiry reconstruction calls=%d callback_nil=%t", queue.scheduleCalls, queue.callback == nil)
|
|
}
|
|
if wantDelay := committed.Lifecycle.PhaseDeadline.Sub(retryAt); queue.scheduledDelay != wantDelay {
|
|
t.Fatalf("reconstructed phase expiry delay=%s want=%s", queue.scheduledDelay, wantDelay)
|
|
}
|
|
}
|
|
|
|
func TestPublishRuntimeRaidReadyOrdersCurrentEXEPacketsBeforeScheduling(t *testing.T) {
|
|
preparedAt := time.Date(2026, 8, 6, 5, 30, 0, 0, time.UTC)
|
|
wantWriteErr := errors.New("raid prepare-ui write failed")
|
|
connection := &runtimeRaidPhaseTestConn{failAt: 4, writeErr: wantWriteErr}
|
|
queue := &runtimeRaidPhaseTestQueue{now: preparedAt}
|
|
service, raidKey, _ := newRuntimeRaidPhaseActivationTestService(t, preparedAt, connection, queue)
|
|
preparing := cloneRuntimeRaidState(*service.raids[raidKey])
|
|
|
|
if err := service.publishRuntimeRaidReady(preparing); !errors.Is(err, wantWriteErr) {
|
|
t.Fatalf("prepare-start write failure=%v want=%v", err, wantWriteErr)
|
|
}
|
|
if queue.scheduleCalls != 0 {
|
|
t.Fatalf("ready timer scheduled before all packets succeeded: calls=%d", queue.scheduleCalls)
|
|
}
|
|
prepareState, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
if prepareState.Header.MsgID != raid.RaidStateMsgID ||
|
|
!bytes.Equal(prepareState.Body, raid.BuildRaidStateBody(raid.RaidState{State: runtimeRaidPrepareState, Argument: 0})) {
|
|
t.Fatalf("partial ready sequence state=%#04x body=%x", prepareState.Header.MsgID, prepareState.Body)
|
|
}
|
|
timer, rest := splitGameServerUpperPacket(t, rest)
|
|
if timer.Header.MsgID != raid.RaidTimerMsgID {
|
|
t.Fatalf("partial ready sequence timer=%#04x body=%x", timer.Header.MsgID, timer.Body)
|
|
}
|
|
remain, rest := splitGameServerUpperPacket(t, rest)
|
|
if remain.Header.MsgID != raid.RaidRemainTimeMsgID || len(rest) != 0 {
|
|
t.Fatalf("partial ready sequence remain=%#04x trailing=%d", remain.Header.MsgID, len(rest))
|
|
}
|
|
connection.write.Reset()
|
|
|
|
if err := service.publishRuntimeRaidReady(preparing); err != nil {
|
|
t.Fatalf("ready sequence retry: %v", err)
|
|
}
|
|
if rest := assertRuntimeRaidReadyTimer(t, connection.write.Bytes(), raidKey); len(rest) != 0 {
|
|
t.Fatalf("ready sequence trailing bytes=%d", len(rest))
|
|
}
|
|
if queue.scheduleCalls != 1 || queue.scheduledDelay != 3*time.Second {
|
|
t.Fatalf("ready scheduling calls=%d delay=%s", queue.scheduleCalls, queue.scheduledDelay)
|
|
}
|
|
}
|
|
|
|
func TestActivateRuntimeRaidSinglePhaseBootstrapsCurrentClientStartNode(t *testing.T) {
|
|
preparedAt := time.Date(2026, 8, 8, 4, 0, 0, 0, time.UTC)
|
|
config := runtimeRaidBridgeTestAntonConfig()
|
|
config.SingleMode = &raid.AntonSingleModeConfig{
|
|
Name: "single",
|
|
TimeLimitSeconds: 2100,
|
|
Phase: raid.AntonPhase{Dungeons: []raid.AntonDungeonInfo{
|
|
{Index: 5314, InitState: raid.AntonDungeonOpen, HP: 1, PartyMax: 0},
|
|
{Index: 5315, InitState: raid.AntonDungeonHide, HP: 1, PartyMax: 0},
|
|
}},
|
|
}
|
|
lifecycle, err := newRuntimeRaidLifecycleStateFromAntonModeAndType(config, preparedAt, true, 0)
|
|
if err != nil {
|
|
t.Fatalf("create single lifecycle: %v", err)
|
|
}
|
|
member := runtimeRaidMemberState{CharID: 1001, Name: "leader", GroupIndex: 1, SlotOrder: 0}
|
|
connection := &runtimeRaidPhaseTestConn{}
|
|
queue := &runtimeRaidPhaseTestQueue{now: lifecycle.ReadyDeadline}
|
|
const raidKey = uint32(0x00c803e9)
|
|
service := &Service{
|
|
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
|
gameplayTimers: queue,
|
|
raids: map[uint32]*runtimeRaidState{
|
|
raidKey: {
|
|
RaidKey: raidKey,
|
|
LeaderID: member.CharID,
|
|
AntonConfig: config,
|
|
TailFlag: 1,
|
|
Members: []runtimeRaidMemberState{member},
|
|
FrozenMembers: []runtimeRaidMemberState{member},
|
|
Started: true,
|
|
EntryCostPaid: true,
|
|
ChargedMembers: []uint16{member.CharID},
|
|
StartAttempt: 1,
|
|
Lifecycle: lifecycle,
|
|
},
|
|
},
|
|
}
|
|
session := &gameSession{conn: connection, connID: "raid-single-phase-runtime-test", channel: channelcatalog.Channel{ID: 200, Type: 23, Name: "anton", Port: 10038}}
|
|
service.bindGameSessionCharacter(session, member.CharID)
|
|
|
|
if _, err := service.activateRuntimeRaidPhase(raidKey, runtimeRaidDungeonPhaseOne, lifecycle.ReadyDeadline, false); err != nil {
|
|
t.Fatalf("activate single phase: %v", err)
|
|
}
|
|
wantIDs := []uint16{
|
|
raid.RaidObjectMsgID,
|
|
raid.RaidBuffMsgID,
|
|
raid.RaidSymbolMsgID,
|
|
raid.RaidDungeonStateMsgID,
|
|
raid.RaidStateMsgID,
|
|
raid.RaidDungeonChangeMsgID,
|
|
raid.RaidTimerMsgID,
|
|
raid.RaidRemainTimeMsgID,
|
|
}
|
|
wantDungeonBody, err := raid.BuildRaidDungeonStateBody(raid.RaidDungeonStateSnapshot{Rows: []raid.RaidDungeonStateRow{
|
|
{DungeonID: 210, State: 0},
|
|
{DungeonID: 211, State: 2},
|
|
{DungeonID: 212, State: 2},
|
|
{DungeonID: 213, State: 2},
|
|
{DungeonID: 214, State: 2},
|
|
{DungeonID: 215, State: 2},
|
|
{DungeonID: 216, State: 2},
|
|
{DungeonID: 218, State: 2},
|
|
{DungeonID: 219, State: 2},
|
|
{DungeonID: 220, State: 2},
|
|
{DungeonID: 221, State: 2},
|
|
{DungeonID: 222, State: 2},
|
|
{DungeonID: 223, State: 2},
|
|
{DungeonID: 224, State: 2},
|
|
{DungeonID: 5314, State: 0},
|
|
{DungeonID: 5315, State: 2},
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("build expected single dungeon state: %v", err)
|
|
}
|
|
stream := connection.write.Bytes()
|
|
for index, wantID := range wantIDs {
|
|
packet, rest := splitGameServerUpperPacket(t, stream)
|
|
stream = rest
|
|
if packet.Header.Classification != 0 || packet.Header.MsgID != wantID {
|
|
t.Fatalf("single phase packet %d class=%d msg=%#04x want=%#04x", index, packet.Header.Classification, packet.Header.MsgID, wantID)
|
|
}
|
|
switch wantID {
|
|
case raid.RaidDungeonStateMsgID:
|
|
if !bytes.Equal(packet.Body, wantDungeonBody) {
|
|
t.Fatalf("single dungeon state body=%x want=%x", packet.Body, wantDungeonBody)
|
|
}
|
|
case raid.RaidDungeonChangeMsgID:
|
|
want := []byte{0xd2, 0, 0, 0, 0, 0}
|
|
if !bytes.Equal(packet.Body, want) {
|
|
t.Fatalf("single start node refresh=%x want=%x", packet.Body, want)
|
|
}
|
|
}
|
|
}
|
|
if len(stream) != 0 {
|
|
t.Fatalf("single phase trailing bytes=%d", len(stream))
|
|
}
|
|
|
|
connection.write.Reset()
|
|
if err := service.sendRuntimeRaidSelectorSnapshot(session, "single_selector_reopen"); err != nil {
|
|
t.Fatalf("reopen single selector: %v", err)
|
|
}
|
|
wantSelectorIDs := []uint16{
|
|
raid.RaidObjectMsgID,
|
|
raid.RaidMemberRefreshMsgID,
|
|
raid.RaidSymbolMsgID,
|
|
raid.RaidDungeonStateMsgID,
|
|
raid.RaidTimerMsgID,
|
|
raid.RaidRemainTimeMsgID,
|
|
}
|
|
stream = connection.write.Bytes()
|
|
for index, wantID := range wantSelectorIDs {
|
|
packet, rest := splitGameServerUpperPacket(t, stream)
|
|
stream = rest
|
|
if packet.Header.Classification != 0 || packet.Header.MsgID != wantID {
|
|
t.Fatalf("single selector packet %d class=%d msg=%#04x want=%#04x", index, packet.Header.Classification, packet.Header.MsgID, wantID)
|
|
}
|
|
if packet.Header.MsgID == raid.RaidStateMsgID {
|
|
t.Fatalf("single selector replayed one-shot raid state=%x", packet.Body)
|
|
}
|
|
if packet.Header.MsgID == raid.RaidDungeonStateMsgID && !bytes.Equal(packet.Body, wantDungeonBody) {
|
|
t.Fatalf("single selector dungeon state body=%x want=%x", packet.Body, wantDungeonBody)
|
|
}
|
|
if packet.Header.MsgID == raid.RaidParticipationMsgID || packet.Header.MsgID == raid.RaidDungeonChangeMsgID {
|
|
t.Fatalf("single selector replayed one-shot bootstrap packet %#04x", packet.Header.MsgID)
|
|
}
|
|
}
|
|
if len(stream) != 0 {
|
|
t.Fatalf("single selector trailing bytes=%d", len(stream))
|
|
}
|
|
}
|
|
|
|
func TestRuntimeRaidObjectMode0PreservesLifecycleForSelectorReplay(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
status runtimeRaidLifecycleStatus
|
|
phase byte
|
|
wantState byte
|
|
wantPhase byte
|
|
}{
|
|
{name: "created", status: runtimeRaidLifecycleCreated, wantPhase: 0xff},
|
|
{name: "preparing", status: runtimeRaidLifecyclePreparing, phase: runtimeRaidDungeonPhaseOne, wantPhase: 0xff},
|
|
{name: "phase_one_active", status: runtimeRaidLifecyclePhaseActive, phase: runtimeRaidDungeonPhaseOne, wantState: 2, wantPhase: 0},
|
|
{name: "phase_two_active", status: runtimeRaidLifecyclePhaseActive, phase: runtimeRaidDungeonPhaseTwo, wantState: 2, wantPhase: 1},
|
|
{name: "awaiting_movie", status: runtimeRaidLifecycleAwaitingMovie, phase: runtimeRaidDungeonPhaseOne, wantState: 3, wantPhase: 0},
|
|
{name: "awaiting_rewards", status: runtimeRaidLifecycleAwaitingRewards, phase: runtimeRaidDungeonPhaseOne, wantState: 5, wantPhase: 0},
|
|
{name: "standby", status: runtimeRaidLifecycleStandby, phase: runtimeRaidDungeonPhaseOne, wantState: 5, wantPhase: 0},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
object := runtimeRaidObjectMode0(runtimeRaidState{
|
|
RaidKey: 0x00170001,
|
|
LeaderID: 1001,
|
|
TailFlag: 1,
|
|
Members: []runtimeRaidMemberState{{
|
|
CharID: 1001,
|
|
Name: "leader",
|
|
}},
|
|
Lifecycle: runtimeRaidLifecycleState{Status: test.status, Phase: test.phase},
|
|
})
|
|
if object.ObjectField28 != test.wantState || object.ObjectField30 != test.wantPhase {
|
|
t.Fatalf("object lifecycle=%d/%d want=%d/%d", object.ObjectField28, object.ObjectField30, test.wantState, test.wantPhase)
|
|
}
|
|
if object.ObjectField94 != 1 || len(object.Members) != 1 {
|
|
t.Fatalf("single/member projection field94=%d members=%d", object.ObjectField94, len(object.Members))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func newRuntimeRaidPhaseActivationTestService(
|
|
t *testing.T,
|
|
preparedAt time.Time,
|
|
connection *runtimeRaidPhaseTestConn,
|
|
queue *runtimeRaidPhaseTestQueue,
|
|
) (*Service, uint32, time.Time) {
|
|
t.Helper()
|
|
const raidKey = uint32(0x00c803e9)
|
|
config := runtimeRaidBridgeTestAntonConfig()
|
|
lifecycle, err := newRuntimeRaidLifecycleStateFromAnton(config, preparedAt)
|
|
if err != nil {
|
|
t.Fatalf("prepare raid lifecycle: %v", err)
|
|
}
|
|
member := runtimeRaidMemberState{CharID: 1001, Name: "leader", GroupIndex: 1, SlotOrder: 0}
|
|
service := &Service{
|
|
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
|
gameplayTimers: queue,
|
|
raids: map[uint32]*runtimeRaidState{
|
|
raidKey: {
|
|
RaidKey: raidKey,
|
|
LeaderID: member.CharID,
|
|
AntonConfig: config,
|
|
Members: []runtimeRaidMemberState{member},
|
|
FrozenMembers: []runtimeRaidMemberState{member},
|
|
Started: true,
|
|
EntryCostPaid: true,
|
|
ChargedMembers: []uint16{member.CharID},
|
|
StartAttempt: 1,
|
|
Lifecycle: lifecycle,
|
|
},
|
|
},
|
|
}
|
|
session := &gameSession{
|
|
conn: connection,
|
|
connID: "raid-phase-runtime-test",
|
|
channel: channelcatalog.Channel{ID: 200, Type: 23, Name: "anton", Port: 10038},
|
|
}
|
|
service.bindGameSessionCharacter(session, member.CharID)
|
|
return service, raidKey, lifecycle.ReadyDeadline
|
|
}
|
|
|
|
// assertRuntimeRaidPhaseActivationPackets pins the raid wire sequence of a phase
|
|
// activation. The stream is filtered rather than read positionally, because
|
|
// activation also re-asserts the squads' ordinary-party projection
|
|
// (op9 -> op153) and installs each squad mate's actor record, and neither belongs
|
|
// to the raid sequence this helper owns.
|
|
func assertRuntimeRaidPhaseActivationPackets(t *testing.T, raw []byte, phase byte, wantSeconds uint32) {
|
|
t.Helper()
|
|
wantIDs := []uint16{
|
|
raid.RaidObjectMsgID,
|
|
raid.RaidBuffMsgID,
|
|
raid.RaidSymbolMsgID,
|
|
raid.RaidDungeonStateMsgID,
|
|
raid.RaidStateMsgID,
|
|
raid.RaidTimerMsgID,
|
|
raid.RaidRemainTimeMsgID,
|
|
}
|
|
packets := make([]runtimeRaidRejoinExpectedPacket, 0, len(wantIDs))
|
|
for len(raw) != 0 {
|
|
packet, rest := splitGameServerUpperPacket(t, raw)
|
|
raw = rest
|
|
if !runtimeRaidRejoinIsRaidPacket(packet.Header.MsgID) {
|
|
continue
|
|
}
|
|
packets = append(packets, runtimeRaidRejoinExpectedPacket{
|
|
msgID: packet.Header.MsgID,
|
|
classification: packet.Header.Classification,
|
|
body: append([]byte(nil), packet.Body...),
|
|
})
|
|
}
|
|
if len(packets) != len(wantIDs) {
|
|
t.Fatalf("phase activation raid packets=%d want=%d", len(packets), len(wantIDs))
|
|
}
|
|
for index, wantID := range wantIDs {
|
|
packet := packets[index]
|
|
if packet.classification != 0 || packet.msgID != wantID {
|
|
t.Fatalf("phase packet %d class=%d msg=%#04x want=%#04x", index, packet.classification, packet.msgID, wantID)
|
|
}
|
|
if wantID == raid.RaidObjectMsgID && len(packet.body) == 0 {
|
|
t.Fatalf("phase activation emitted an empty raid object")
|
|
}
|
|
if packet.msgID == raid.RaidParticipationMsgID {
|
|
t.Fatalf("phase activation emitted empty participation packet 0x0248")
|
|
}
|
|
switch wantID {
|
|
case raid.RaidStateMsgID:
|
|
wantBody := []byte{runtimeRaidPhaseStartState, phase - 1}
|
|
if !bytes.Equal(packet.body, wantBody) {
|
|
t.Fatalf("phase state body=%x want=%x", packet.body, wantBody)
|
|
}
|
|
case raid.RaidTimerMsgID:
|
|
if len(packet.body) != 13 || packet.body[0] != 0 ||
|
|
binary.LittleEndian.Uint32(packet.body[1:5]) != runtimeRaidTopTimerKey ||
|
|
binary.LittleEndian.Uint32(packet.body[5:9]) == 0 ||
|
|
binary.LittleEndian.Uint32(packet.body[9:13]) == 0 {
|
|
t.Fatalf("phase timer body=%x", packet.body)
|
|
}
|
|
case raid.RaidRemainTimeMsgID:
|
|
if len(packet.body) != 5 || packet.body[0] != 0 || binary.LittleEndian.Uint32(packet.body[1:]) != wantSeconds {
|
|
t.Fatalf("phase remaining body=%x want_seconds=%d", packet.body, wantSeconds)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type runtimeRaidPhaseTestConn struct {
|
|
bufferConn
|
|
remainingFailures int
|
|
writeCalls int
|
|
failAt int
|
|
writeErr error
|
|
}
|
|
|
|
func (connection *runtimeRaidPhaseTestConn) Write(data []byte) (int, error) {
|
|
connection.writeCalls++
|
|
if connection.failAt > 0 && connection.writeCalls == connection.failAt {
|
|
return 0, connection.writeErr
|
|
}
|
|
if connection.remainingFailures > 0 {
|
|
connection.remainingFailures--
|
|
return 0, connection.writeErr
|
|
}
|
|
return connection.bufferConn.Write(data)
|
|
}
|
|
|
|
type runtimeRaidPhaseTestQueue struct {
|
|
now time.Time
|
|
remainingScheduleFailures int
|
|
scheduleErr error
|
|
scheduleCalls int
|
|
scheduledName string
|
|
scheduledDelay time.Duration
|
|
callback timequeue.Callback
|
|
}
|
|
|
|
func (queue *runtimeRaidPhaseTestQueue) Now() time.Time {
|
|
return queue.now
|
|
}
|
|
|
|
func (queue *runtimeRaidPhaseTestQueue) ScheduleAfter(
|
|
name string,
|
|
delay time.Duration,
|
|
callback timequeue.Callback,
|
|
) error {
|
|
queue.scheduleCalls++
|
|
queue.scheduledName = name
|
|
queue.scheduledDelay = delay
|
|
if queue.remainingScheduleFailures > 0 {
|
|
queue.remainingScheduleFailures--
|
|
return queue.scheduleErr
|
|
}
|
|
queue.callback = callback
|
|
return nil
|
|
}
|
|
|
|
func (queue *runtimeRaidPhaseTestQueue) RegisterDaily(string, int, int, timequeue.Callback) error {
|
|
return nil
|
|
}
|
|
|
|
func (queue *runtimeRaidPhaseTestQueue) Cancel(string) bool {
|
|
return false
|
|
}
|
|
|
|
func (queue *runtimeRaidPhaseTestQueue) Start(context.Context) {}
|