mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 15:43:49 +01:00
1088 lines
24 KiB
Go
1088 lines
24 KiB
Go
package channelserver
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestCharacterSaveDataPersistenceEdgeCases tests edge cases in character savedata persistence
|
|
func TestCharacterSaveDataPersistenceEdgeCases(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
charID uint32
|
|
charName string
|
|
isNew bool
|
|
playtime uint32
|
|
wantValid bool
|
|
}{
|
|
{
|
|
name: "valid_new_character",
|
|
charID: 1,
|
|
charName: "TestChar",
|
|
isNew: true,
|
|
playtime: 0,
|
|
wantValid: true,
|
|
},
|
|
{
|
|
name: "existing_character_with_playtime",
|
|
charID: 100,
|
|
charName: "ExistingChar",
|
|
isNew: false,
|
|
playtime: 3600,
|
|
wantValid: true,
|
|
},
|
|
{
|
|
name: "character_max_playtime",
|
|
charID: 999,
|
|
charName: "MaxPlaytime",
|
|
isNew: false,
|
|
playtime: 4294967295, // Max uint32
|
|
wantValid: true,
|
|
},
|
|
{
|
|
name: "character_zero_id",
|
|
charID: 0,
|
|
charName: "ZeroID",
|
|
isNew: true,
|
|
playtime: 0,
|
|
wantValid: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
savedata := &CharacterSaveData{
|
|
CharID: tt.charID,
|
|
Name: tt.charName,
|
|
IsNewCharacter: tt.isNew,
|
|
Playtime: tt.playtime,
|
|
Pointers: make(map[SavePointer]int),
|
|
}
|
|
|
|
// Verify data integrity
|
|
if savedata.CharID != tt.charID {
|
|
t.Errorf("character ID mismatch: got %d, want %d", savedata.CharID, tt.charID)
|
|
}
|
|
|
|
if savedata.Name != tt.charName {
|
|
t.Errorf("character name mismatch: got %s, want %s", savedata.Name, tt.charName)
|
|
}
|
|
|
|
if savedata.Playtime != tt.playtime {
|
|
t.Errorf("playtime mismatch: got %d, want %d", savedata.Playtime, tt.playtime)
|
|
}
|
|
|
|
isValid := tt.charID > 0 && len(tt.charName) > 0
|
|
if isValid != tt.wantValid {
|
|
t.Errorf("validity check failed: got %v, want %v", isValid, tt.wantValid)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSaveDataCompressionRoundTrip tests compression/decompression edge cases
|
|
func TestSaveDataCompressionRoundTrip(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
dataSize int
|
|
dataPattern byte
|
|
compresses bool
|
|
}{
|
|
{
|
|
name: "empty_data",
|
|
dataSize: 0,
|
|
dataPattern: 0x00,
|
|
compresses: true,
|
|
},
|
|
{
|
|
name: "small_data",
|
|
dataSize: 10,
|
|
dataPattern: 0xFF,
|
|
compresses: false, // Small data may not compress well
|
|
},
|
|
{
|
|
name: "highly_repetitive_data",
|
|
dataSize: 1000,
|
|
dataPattern: 0xAA,
|
|
compresses: true, // Highly repetitive should compress
|
|
},
|
|
{
|
|
name: "random_data",
|
|
dataSize: 500,
|
|
dataPattern: 0x00, // Will be varied by position
|
|
compresses: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Create test data
|
|
data := make([]byte, tt.dataSize)
|
|
for i := 0; i < tt.dataSize; i++ {
|
|
if tt.dataPattern == 0x00 {
|
|
// Vary pattern for "random" data
|
|
data[i] = byte((i * 17) % 256)
|
|
} else {
|
|
data[i] = tt.dataPattern
|
|
}
|
|
}
|
|
|
|
// Verify data integrity after theoretical compression
|
|
if len(data) != tt.dataSize {
|
|
t.Errorf("data size mismatch after preparation: got %d, want %d", len(data), tt.dataSize)
|
|
}
|
|
|
|
// Verify data is not corrupted
|
|
for i := 0; i < tt.dataSize; i++ {
|
|
expectedByte := data[i]
|
|
if data[i] != expectedByte {
|
|
t.Errorf("data corruption at position %d", i)
|
|
break
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSaveDataPointerHandling tests edge cases in save data pointer management
|
|
func TestSaveDataPointerHandling(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
pointerCount int
|
|
maxPointerValue int
|
|
valid bool
|
|
}{
|
|
{
|
|
name: "no_pointers",
|
|
pointerCount: 0,
|
|
maxPointerValue: 0,
|
|
valid: true,
|
|
},
|
|
{
|
|
name: "single_pointer",
|
|
pointerCount: 1,
|
|
maxPointerValue: 100,
|
|
valid: true,
|
|
},
|
|
{
|
|
name: "multiple_pointers",
|
|
pointerCount: 10,
|
|
maxPointerValue: 5000,
|
|
valid: true,
|
|
},
|
|
{
|
|
name: "max_pointers",
|
|
pointerCount: 100,
|
|
maxPointerValue: 1000000,
|
|
valid: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
savedata := &CharacterSaveData{
|
|
CharID: 1,
|
|
Pointers: make(map[SavePointer]int),
|
|
}
|
|
|
|
// Add test pointers
|
|
for i := 0; i < tt.pointerCount; i++ {
|
|
pointer := SavePointer(i % 20) // Cycle through pointer types
|
|
value := (i * 100) % tt.maxPointerValue
|
|
savedata.Pointers[pointer] = value
|
|
}
|
|
|
|
// Verify pointer count
|
|
if len(savedata.Pointers) != tt.pointerCount && tt.pointerCount < 20 {
|
|
t.Errorf("pointer count mismatch: got %d, want %d", len(savedata.Pointers), tt.pointerCount)
|
|
}
|
|
|
|
// Verify pointer values are reasonable
|
|
for ptr, val := range savedata.Pointers {
|
|
if val < 0 || val > tt.maxPointerValue {
|
|
t.Errorf("pointer %v value out of range: %d", ptr, val)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSaveDataGenderHandling tests gender field handling
|
|
func TestSaveDataGenderHandling(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
gender bool
|
|
label string
|
|
}{
|
|
{
|
|
name: "male_character",
|
|
gender: false,
|
|
label: "male",
|
|
},
|
|
{
|
|
name: "female_character",
|
|
gender: true,
|
|
label: "female",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
savedata := &CharacterSaveData{
|
|
CharID: 1,
|
|
Gender: tt.gender,
|
|
}
|
|
|
|
if savedata.Gender != tt.gender {
|
|
t.Errorf("gender mismatch: got %v, want %v", savedata.Gender, tt.gender)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSaveDataWeaponTypeHandling tests weapon type field handling
|
|
func TestSaveDataWeaponTypeHandling(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
weaponType uint8
|
|
valid bool
|
|
}{
|
|
{
|
|
name: "weapon_type_0",
|
|
weaponType: 0,
|
|
valid: true,
|
|
},
|
|
{
|
|
name: "weapon_type_middle",
|
|
weaponType: 5,
|
|
valid: true,
|
|
},
|
|
{
|
|
name: "weapon_type_max",
|
|
weaponType: 255,
|
|
valid: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
savedata := &CharacterSaveData{
|
|
CharID: 1,
|
|
WeaponType: tt.weaponType,
|
|
}
|
|
|
|
if savedata.WeaponType != tt.weaponType {
|
|
t.Errorf("weapon type mismatch: got %d, want %d", savedata.WeaponType, tt.weaponType)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSaveDataRPHandling tests RP (resource points) handling
|
|
func TestSaveDataRPHandling(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
rpPoints uint16
|
|
valid bool
|
|
}{
|
|
{
|
|
name: "zero_rp",
|
|
rpPoints: 0,
|
|
valid: true,
|
|
},
|
|
{
|
|
name: "moderate_rp",
|
|
rpPoints: 1000,
|
|
valid: true,
|
|
},
|
|
{
|
|
name: "max_rp",
|
|
rpPoints: 65535, // Max uint16
|
|
valid: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
savedata := &CharacterSaveData{
|
|
CharID: 1,
|
|
RP: tt.rpPoints,
|
|
}
|
|
|
|
if savedata.RP != tt.rpPoints {
|
|
t.Errorf("RP mismatch: got %d, want %d", savedata.RP, tt.rpPoints)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSaveDataHousingDataHandling tests various housing/decorative data fields
|
|
func TestSaveDataHousingDataHandling(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
houseTier []byte
|
|
houseData []byte
|
|
bookshelfData []byte
|
|
galleryData []byte
|
|
validEmpty bool
|
|
}{
|
|
{
|
|
name: "all_empty_housing",
|
|
houseTier: []byte{},
|
|
houseData: []byte{},
|
|
bookshelfData: []byte{},
|
|
galleryData: []byte{},
|
|
validEmpty: true,
|
|
},
|
|
{
|
|
name: "with_house_tier",
|
|
houseTier: []byte{0x01, 0x02, 0x03},
|
|
houseData: []byte{},
|
|
bookshelfData: []byte{},
|
|
galleryData: []byte{},
|
|
validEmpty: false,
|
|
},
|
|
{
|
|
name: "all_housing_data",
|
|
houseTier: []byte{0xFF},
|
|
houseData: []byte{0xAA, 0xBB},
|
|
bookshelfData: []byte{0xCC, 0xDD, 0xEE},
|
|
galleryData: []byte{0x11, 0x22, 0x33, 0x44},
|
|
validEmpty: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
savedata := &CharacterSaveData{
|
|
CharID: 1,
|
|
HouseTier: tt.houseTier,
|
|
HouseData: tt.houseData,
|
|
BookshelfData: tt.bookshelfData,
|
|
GalleryData: tt.galleryData,
|
|
}
|
|
|
|
if !bytes.Equal(savedata.HouseTier, tt.houseTier) {
|
|
t.Errorf("house tier mismatch")
|
|
}
|
|
|
|
if !bytes.Equal(savedata.HouseData, tt.houseData) {
|
|
t.Errorf("house data mismatch")
|
|
}
|
|
|
|
if !bytes.Equal(savedata.BookshelfData, tt.bookshelfData) {
|
|
t.Errorf("bookshelf data mismatch")
|
|
}
|
|
|
|
if !bytes.Equal(savedata.GalleryData, tt.galleryData) {
|
|
t.Errorf("gallery data mismatch")
|
|
}
|
|
|
|
isEmpty := len(tt.houseTier) == 0 && len(tt.houseData) == 0 && len(tt.bookshelfData) == 0 && len(tt.galleryData) == 0
|
|
if isEmpty != tt.validEmpty {
|
|
t.Errorf("empty check mismatch: got %v, want %v", isEmpty, tt.validEmpty)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSaveDataFieldDataHandling tests tore and garden data
|
|
func TestSaveDataFieldDataHandling(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
toreData []byte
|
|
gardenData []byte
|
|
}{
|
|
{
|
|
name: "empty_field_data",
|
|
toreData: []byte{},
|
|
gardenData: []byte{},
|
|
},
|
|
{
|
|
name: "with_tore_data",
|
|
toreData: []byte{0x01, 0x02, 0x03, 0x04},
|
|
gardenData: []byte{},
|
|
},
|
|
{
|
|
name: "with_garden_data",
|
|
toreData: []byte{},
|
|
gardenData: []byte{0xFF, 0xFE, 0xFD},
|
|
},
|
|
{
|
|
name: "both_field_data",
|
|
toreData: []byte{0xAA, 0xBB},
|
|
gardenData: []byte{0xCC, 0xDD, 0xEE},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
savedata := &CharacterSaveData{
|
|
CharID: 1,
|
|
ToreData: tt.toreData,
|
|
GardenData: tt.gardenData,
|
|
}
|
|
|
|
if !bytes.Equal(savedata.ToreData, tt.toreData) {
|
|
t.Errorf("tore data mismatch")
|
|
}
|
|
|
|
if !bytes.Equal(savedata.GardenData, tt.gardenData) {
|
|
t.Errorf("garden data mismatch")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSaveDataIntegrity tests data integrity after construction
|
|
func TestSaveDataIntegrity(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
runs int
|
|
verify func(*CharacterSaveData) bool
|
|
}{
|
|
{
|
|
name: "pointers_immutable",
|
|
runs: 10,
|
|
verify: func(sd *CharacterSaveData) bool {
|
|
initialPointers := len(sd.Pointers)
|
|
sd.Pointers[SavePointer(0)] = 100
|
|
return len(sd.Pointers) == initialPointers+1
|
|
},
|
|
},
|
|
{
|
|
name: "char_id_consistency",
|
|
runs: 10,
|
|
verify: func(sd *CharacterSaveData) bool {
|
|
id := sd.CharID
|
|
return id == sd.CharID
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
for run := 0; run < tt.runs; run++ {
|
|
savedata := &CharacterSaveData{
|
|
CharID: uint32(run + 1),
|
|
Name: "TestChar",
|
|
Pointers: make(map[SavePointer]int),
|
|
}
|
|
|
|
if !tt.verify(savedata) {
|
|
t.Errorf("integrity check failed for run %d", run)
|
|
break
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSaveDataDiffTracking tests tracking of differential updates
|
|
func TestSaveDataDiffTracking(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
isDiffMode bool
|
|
}{
|
|
{
|
|
name: "full_blob_mode",
|
|
isDiffMode: false,
|
|
},
|
|
{
|
|
name: "differential_mode",
|
|
isDiffMode: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Create two savedata instances
|
|
savedata1 := &CharacterSaveData{
|
|
CharID: 1,
|
|
Name: "Char1",
|
|
RP: 1000,
|
|
}
|
|
|
|
savedata2 := &CharacterSaveData{
|
|
CharID: 1,
|
|
Name: "Char1",
|
|
RP: 2000, // Different RP
|
|
}
|
|
|
|
// In differential mode, only changed fields would be sent
|
|
isDifferent := savedata1.RP != savedata2.RP
|
|
|
|
if !isDifferent && tt.isDiffMode {
|
|
t.Error("should detect difference in differential mode")
|
|
}
|
|
|
|
if isDifferent {
|
|
// Expected when there are differences
|
|
if !tt.isDiffMode && savedata1.CharID != savedata2.CharID {
|
|
t.Error("full blob mode should preserve all data")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSaveDataBoundaryValues tests boundary value handling
|
|
func TestSaveDataBoundaryValues(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
charID uint32
|
|
playtime uint32
|
|
rp uint16
|
|
}{
|
|
{
|
|
name: "min_values",
|
|
charID: 1, // Minimum valid ID
|
|
playtime: 0,
|
|
rp: 0,
|
|
},
|
|
{
|
|
name: "max_uint32_playtime",
|
|
charID: 100,
|
|
playtime: 4294967295,
|
|
rp: 0,
|
|
},
|
|
{
|
|
name: "max_uint16_rp",
|
|
charID: 100,
|
|
playtime: 0,
|
|
rp: 65535,
|
|
},
|
|
{
|
|
name: "all_max_values",
|
|
charID: 4294967295,
|
|
playtime: 4294967295,
|
|
rp: 65535,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
savedata := &CharacterSaveData{
|
|
CharID: tt.charID,
|
|
Playtime: tt.playtime,
|
|
RP: tt.rp,
|
|
}
|
|
|
|
if savedata.CharID != tt.charID {
|
|
t.Errorf("char ID boundary check failed")
|
|
}
|
|
|
|
if savedata.Playtime != tt.playtime {
|
|
t.Errorf("playtime boundary check failed")
|
|
}
|
|
|
|
if savedata.RP != tt.rp {
|
|
t.Errorf("RP boundary check failed")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSaveDataSerialization tests savedata can be serialized to binary format
|
|
func TestSaveDataSerialization(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
charID uint32
|
|
playtime uint32
|
|
}{
|
|
{
|
|
name: "simple_serialization",
|
|
charID: 1,
|
|
playtime: 100,
|
|
},
|
|
{
|
|
name: "large_playtime",
|
|
charID: 999,
|
|
playtime: 1000000,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
savedata := &CharacterSaveData{
|
|
CharID: tt.charID,
|
|
Playtime: tt.playtime,
|
|
}
|
|
|
|
// Simulate binary serialization
|
|
buf := new(bytes.Buffer)
|
|
binary.Write(buf, binary.LittleEndian, savedata.CharID)
|
|
binary.Write(buf, binary.LittleEndian, savedata.Playtime)
|
|
|
|
// Should have 8 bytes (4 + 4)
|
|
if buf.Len() != 8 {
|
|
t.Errorf("serialized size mismatch: got %d, want 8", buf.Len())
|
|
}
|
|
|
|
// Deserialize and verify
|
|
data := buf.Bytes()
|
|
var charID uint32
|
|
var playtime uint32
|
|
binary.Read(bytes.NewReader(data), binary.LittleEndian, &charID)
|
|
binary.Read(bytes.NewReader(data[4:]), binary.LittleEndian, &playtime)
|
|
|
|
if charID != tt.charID || playtime != tt.playtime {
|
|
t.Error("serialization round-trip failed")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSaveDataTimestampHandling tests timestamp field handling for data freshness
|
|
func TestSaveDataTimestampHandling(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ageSeconds int
|
|
expectFresh bool
|
|
}{
|
|
{
|
|
name: "just_saved",
|
|
ageSeconds: 0,
|
|
expectFresh: true,
|
|
},
|
|
{
|
|
name: "recent_save",
|
|
ageSeconds: 60,
|
|
expectFresh: true,
|
|
},
|
|
{
|
|
name: "old_save",
|
|
ageSeconds: 86400, // 1 day old
|
|
expectFresh: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
now := time.Now()
|
|
lastSave := now.Add(time.Duration(-tt.ageSeconds) * time.Second)
|
|
|
|
// Simulate freshness check
|
|
age := now.Sub(lastSave)
|
|
isFresh := age < 3600*time.Second // 1 hour
|
|
|
|
if isFresh != tt.expectFresh {
|
|
t.Errorf("freshness check failed: got %v, want %v", isFresh, tt.expectFresh)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDataCorruptionRecovery tests recovery from corrupted savedata
|
|
func TestDataCorruptionRecovery(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
originalData []byte
|
|
corruptedData []byte
|
|
canRecover bool
|
|
recoveryMethod string
|
|
}{
|
|
{
|
|
name: "minor_bit_flip",
|
|
originalData: []byte{0xFF, 0xFF, 0xFF, 0xFF},
|
|
corruptedData: []byte{0xFF, 0xFE, 0xFF, 0xFF}, // One bit flipped
|
|
canRecover: true,
|
|
recoveryMethod: "checksum_validation",
|
|
},
|
|
{
|
|
name: "single_byte_corruption",
|
|
originalData: []byte{0x00, 0x01, 0x02, 0x03, 0x04},
|
|
corruptedData: []byte{0x00, 0xFF, 0x02, 0x03, 0x04}, // Middle byte corrupted
|
|
canRecover: true,
|
|
recoveryMethod: "crc32_check",
|
|
},
|
|
{
|
|
name: "data_truncation",
|
|
originalData: []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05},
|
|
corruptedData: []byte{0x00, 0x01}, // Truncated
|
|
canRecover: true,
|
|
recoveryMethod: "length_validation",
|
|
},
|
|
{
|
|
name: "complete_garbage",
|
|
originalData: []byte{0x00, 0x01, 0x02},
|
|
corruptedData: []byte{}, // Empty/no data
|
|
canRecover: false,
|
|
recoveryMethod: "none",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Simulate corruption detection
|
|
isCorrupted := !bytes.Equal(tt.originalData, tt.corruptedData)
|
|
|
|
if isCorrupted && tt.canRecover {
|
|
// Try recovery validation based on method
|
|
canRecover := false
|
|
switch tt.recoveryMethod {
|
|
case "checksum_validation":
|
|
// Simple checksum check
|
|
canRecover = len(tt.corruptedData) == len(tt.originalData)
|
|
case "crc32_check":
|
|
// Length should match
|
|
canRecover = len(tt.corruptedData) == len(tt.originalData)
|
|
case "length_validation":
|
|
// Can recover if we have partial data
|
|
canRecover = len(tt.corruptedData) > 0
|
|
}
|
|
|
|
if !canRecover && tt.canRecover {
|
|
t.Errorf("failed to recover from corruption using %s", tt.recoveryMethod)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestChecksumValidation tests savedata checksum validation
|
|
func TestChecksumValidation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
data []byte
|
|
checksumValid bool
|
|
}{
|
|
{
|
|
name: "valid_checksum",
|
|
data: []byte{0x01, 0x02, 0x03, 0x04},
|
|
checksumValid: true,
|
|
},
|
|
{
|
|
name: "corrupted_data_fails_checksum",
|
|
data: []byte{0xFF, 0xFF, 0xFF, 0xFF},
|
|
checksumValid: true, // Checksum can still be valid, but content is suspicious
|
|
},
|
|
{
|
|
name: "empty_data_valid_checksum",
|
|
data: []byte{},
|
|
checksumValid: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Calculate simple checksum
|
|
var checksum byte
|
|
for _, b := range tt.data {
|
|
checksum ^= b
|
|
}
|
|
|
|
// Verify checksum can be calculated
|
|
_ = (len(tt.data) > 0 && checksum == 0xFF && len(tt.data) == 4 && tt.data[0] == 0xFF)
|
|
// Expected for all 0xFF data
|
|
|
|
// If original passes checksum, verify it's consistent
|
|
checksum2 := byte(0)
|
|
for _, b := range tt.data {
|
|
checksum2 ^= b
|
|
}
|
|
|
|
if checksum != checksum2 {
|
|
t.Error("checksum calculation not consistent")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSaveDataBackupRestoration tests backup and restoration functionality
|
|
func TestSaveDataBackupRestoration(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
originalCharID uint32
|
|
originalPlaytime uint32
|
|
hasBackup bool
|
|
canRestore bool
|
|
}{
|
|
{
|
|
name: "backup_with_restore",
|
|
originalCharID: 1,
|
|
originalPlaytime: 1000,
|
|
hasBackup: true,
|
|
canRestore: true,
|
|
},
|
|
{
|
|
name: "no_backup_available",
|
|
originalCharID: 2,
|
|
originalPlaytime: 2000,
|
|
hasBackup: false,
|
|
canRestore: false,
|
|
},
|
|
{
|
|
name: "backup_corrupt_fallback",
|
|
originalCharID: 3,
|
|
originalPlaytime: 3000,
|
|
hasBackup: true,
|
|
canRestore: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Create original data
|
|
original := &CharacterSaveData{
|
|
CharID: tt.originalCharID,
|
|
Playtime: tt.originalPlaytime,
|
|
}
|
|
|
|
// Create backup
|
|
var backup *CharacterSaveData
|
|
if tt.hasBackup {
|
|
backup = &CharacterSaveData{
|
|
CharID: original.CharID,
|
|
Playtime: original.Playtime,
|
|
}
|
|
}
|
|
|
|
// Simulate data corruption
|
|
original.Playtime = 9999
|
|
|
|
// Try restoration
|
|
if tt.canRestore && backup != nil {
|
|
// Restore from backup
|
|
original.Playtime = backup.Playtime
|
|
}
|
|
|
|
// Verify restoration worked
|
|
if tt.canRestore && backup != nil {
|
|
if original.Playtime != tt.originalPlaytime {
|
|
t.Errorf("restoration failed: got %d, want %d", original.Playtime, tt.originalPlaytime)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSaveDataVersionMigration tests savedata version migration and compatibility
|
|
func TestSaveDataVersionMigration(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
sourceVersion int
|
|
targetVersion int
|
|
canMigrate bool
|
|
dataLoss bool
|
|
}{
|
|
{
|
|
name: "same_version",
|
|
sourceVersion: 1,
|
|
targetVersion: 1,
|
|
canMigrate: true,
|
|
dataLoss: false,
|
|
},
|
|
{
|
|
name: "forward_compatible",
|
|
sourceVersion: 1,
|
|
targetVersion: 2,
|
|
canMigrate: true,
|
|
dataLoss: false,
|
|
},
|
|
{
|
|
name: "backward_compatible",
|
|
sourceVersion: 2,
|
|
targetVersion: 1,
|
|
canMigrate: true,
|
|
dataLoss: true, // Newer fields might be lost
|
|
},
|
|
{
|
|
name: "incompatible_versions",
|
|
sourceVersion: 1,
|
|
targetVersion: 10,
|
|
canMigrate: false,
|
|
dataLoss: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Determine migration compatibility
|
|
canMigrate := false
|
|
dataLoss := false
|
|
|
|
versionDiff := tt.targetVersion - tt.sourceVersion
|
|
if versionDiff == 0 {
|
|
canMigrate = true
|
|
} else if versionDiff == 1 {
|
|
canMigrate = true // Forward migration by one version
|
|
dataLoss = false
|
|
} else if versionDiff < 0 {
|
|
canMigrate = true // Backward migration
|
|
dataLoss = true
|
|
} else if versionDiff > 2 {
|
|
canMigrate = false // Too many versions apart
|
|
dataLoss = true
|
|
}
|
|
|
|
if canMigrate != tt.canMigrate {
|
|
t.Errorf("migration capability mismatch: got %v, want %v", canMigrate, tt.canMigrate)
|
|
}
|
|
|
|
if dataLoss != tt.dataLoss {
|
|
t.Errorf("data loss expectation mismatch: got %v, want %v", dataLoss, tt.dataLoss)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSaveDataRollback tests rollback to previous savedata state
|
|
func TestSaveDataRollback(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
snapshots int
|
|
canRollback bool
|
|
rollbackSteps int
|
|
}{
|
|
{
|
|
name: "single_snapshot",
|
|
snapshots: 1,
|
|
canRollback: false,
|
|
rollbackSteps: 0,
|
|
},
|
|
{
|
|
name: "multiple_snapshots",
|
|
snapshots: 5,
|
|
canRollback: true,
|
|
rollbackSteps: 2,
|
|
},
|
|
{
|
|
name: "many_snapshots",
|
|
snapshots: 100,
|
|
canRollback: true,
|
|
rollbackSteps: 50,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Create snapshot history
|
|
snapshots := make([]*CharacterSaveData, tt.snapshots)
|
|
for i := 0; i < tt.snapshots; i++ {
|
|
snapshots[i] = &CharacterSaveData{
|
|
CharID: 1,
|
|
Playtime: uint32(i * 100),
|
|
}
|
|
}
|
|
|
|
// Can only rollback if we have more than one snapshot
|
|
canRollback := len(snapshots) > 1
|
|
|
|
if canRollback != tt.canRollback {
|
|
t.Errorf("rollback capability mismatch: got %v, want %v", canRollback, tt.canRollback)
|
|
}
|
|
|
|
// Test rollback steps
|
|
if canRollback && tt.rollbackSteps > 0 {
|
|
if tt.rollbackSteps >= len(snapshots) {
|
|
t.Error("rollback steps exceed available snapshots")
|
|
}
|
|
|
|
// Simulate rollback
|
|
currentIdx := len(snapshots) - 1
|
|
targetIdx := currentIdx - tt.rollbackSteps
|
|
if targetIdx >= 0 {
|
|
rolledBackData := snapshots[targetIdx]
|
|
expectedPlaytime := uint32(targetIdx * 100)
|
|
if rolledBackData.Playtime != expectedPlaytime {
|
|
t.Errorf("rollback verification failed: got %d, want %d", rolledBackData.Playtime, expectedPlaytime)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSaveDataValidationOnLoad tests validation when loading savedata
|
|
func TestSaveDataValidationOnLoad(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
charID uint32
|
|
charName string
|
|
isNew bool
|
|
shouldPass bool
|
|
}{
|
|
{
|
|
name: "valid_load",
|
|
charID: 1,
|
|
charName: "TestChar",
|
|
isNew: false,
|
|
shouldPass: true,
|
|
},
|
|
{
|
|
name: "invalid_zero_id",
|
|
charID: 0,
|
|
charName: "TestChar",
|
|
isNew: false,
|
|
shouldPass: false,
|
|
},
|
|
{
|
|
name: "empty_name",
|
|
charID: 1,
|
|
charName: "",
|
|
isNew: true,
|
|
shouldPass: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Validate on load
|
|
isValid := tt.charID > 0 && len(tt.charName) > 0
|
|
|
|
if isValid != tt.shouldPass {
|
|
t.Errorf("validation check failed: got %v, want %v", isValid, tt.shouldPass)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSaveDataConcurrentAccess tests concurrent access to savedata structures
|
|
func TestSaveDataConcurrentAccess(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
concurrentReads int
|
|
concurrentWrites int
|
|
}{
|
|
{
|
|
name: "multiple_readers",
|
|
concurrentReads: 5,
|
|
concurrentWrites: 0,
|
|
},
|
|
{
|
|
name: "multiple_writers",
|
|
concurrentReads: 0,
|
|
concurrentWrites: 3,
|
|
},
|
|
{
|
|
name: "mixed_access",
|
|
concurrentReads: 3,
|
|
concurrentWrites: 2,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// This is a structural test - actual concurrent access would need mutexes
|
|
savedata := &CharacterSaveData{
|
|
CharID: 1,
|
|
Playtime: 0,
|
|
}
|
|
|
|
// Simulate concurrent operations
|
|
totalOps := tt.concurrentReads + tt.concurrentWrites
|
|
if totalOps == 0 {
|
|
t.Skip("no concurrent operations to test")
|
|
}
|
|
|
|
// Verify savedata structure is intact
|
|
if savedata.CharID != 1 {
|
|
t.Error("savedata corrupted by concurrent access test")
|
|
}
|
|
})
|
|
}
|
|
}
|