package transcoder import ( "bytes" "reflect" "strconv" "strings" "testing" model "galaxy/model/report" fbs "galaxy/schema/fbs/report" flatbuffers "github.com/google/flatbuffers/go" "github.com/google/uuid" ) func TestGameReportRequestPayloadRoundTrip(t *testing.T) { t.Parallel() source := &model.GameReportRequest{ GameID: uuid.MustParse("11111111-2222-3333-4444-555555555555"), Turn: 42, } payload, err := GameReportRequestToPayload(source) if err != nil { t.Fatalf("encode game report request: %v", err) } decoded, err := PayloadToGameReportRequest(payload) if err != nil { t.Fatalf("decode game report request: %v", err) } if !reflect.DeepEqual(source, decoded) { t.Fatalf("round-trip mismatch\nsource: %#v\ndecoded: %#v", source, decoded) } } func TestGameReportRequestRejectsEmptyAndNil(t *testing.T) { t.Parallel() if _, err := GameReportRequestToPayload(nil); err == nil { t.Fatalf("expected error encoding nil request") } if _, err := PayloadToGameReportRequest(nil); err == nil { t.Fatalf("expected error decoding empty payload") } } func TestReportToPayloadAndPayloadToReportRoundTrip(t *testing.T) { t.Parallel() source := sampleReport() payload, err := ReportToPayload(source) if err != nil { t.Fatalf("encode report payload: %v", err) } decoded, err := PayloadToReport(payload) if err != nil { t.Fatalf("decode report payload: %v", err) } expected := reportWireClone(t, source) if !reflect.DeepEqual(expected, decoded) { t.Fatalf("round-trip mismatch\nexpected: %#v\ndecoded: %#v", expected, decoded) } } func TestReportToPayloadNilReport(t *testing.T) { t.Parallel() _, err := ReportToPayload(nil) if err == nil { t.Fatal("expected error for nil report") } } func TestPayloadToReportEmptyData(t *testing.T) { t.Parallel() _, err := PayloadToReport(nil) if err == nil { t.Fatal("expected error for empty payload") } } func TestPayloadToReportGarbageDataDoesNotPanic(t *testing.T) { t.Parallel() _, err := PayloadToReport([]byte{0x01, 0x02, 0x03}) if err == nil { t.Fatal("expected error for malformed payload") } } func TestPayloadToReportMissingRequiredLocalGroupID(t *testing.T) { t.Parallel() payload := buildReportPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { class := builder.CreateString("frigate") state := builder.CreateString("orbit") fbs.LocalGroupStart(builder) fbs.LocalGroupAddClass(builder, class) fbs.LocalGroupAddState(builder, state) localGroup := fbs.LocalGroupEnd(builder) fbs.ReportStartLocalGroupVector(builder, 1) builder.PrependUOffsetT(localGroup) localGroups := builder.EndVector(1) fbs.ReportStart(builder) fbs.ReportAddLocalGroup(builder, localGroups) return fbs.ReportEnd(builder) }) _, err := PayloadToReport(payload) if err == nil { t.Fatal("expected error for missing local group id") } if !strings.Contains(err.Error(), "id is missing") { t.Fatalf("unexpected error: %v", err) } } func TestPayloadToReportOverflow(t *testing.T) { t.Parallel() if strconv.IntSize == 64 { t.Skip("uint overflow from uint64 is not possible on 64-bit runtime") } maxUint := uint64(^uint(0)) overflowValue := maxUint + 1 payload := buildReportPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { fbs.ReportStart(builder) fbs.ReportAddTurn(builder, overflowValue) return fbs.ReportEnd(builder) }) _, err := PayloadToReport(payload) if err == nil { t.Fatal("expected overflow error") } if !strings.Contains(err.Error(), "overflows uint") { t.Fatalf("unexpected error: %v", err) } } func TestReportToPayloadDeterministicMapEncoding(t *testing.T) { t.Parallel() report := sampleReport() firstPayload, err := ReportToPayload(report) if err != nil { t.Fatalf("encode report payload: %v", err) } for i := 0; i < 20; i++ { nextPayload, nextErr := ReportToPayload(report) if nextErr != nil { t.Fatalf("encode report payload #%d: %v", i+2, nextErr) } if !bytes.Equal(firstPayload, nextPayload) { t.Fatalf("payload differs between runs at iteration %d", i+2) } } } func TestReportToPayloadFloat32Quantization(t *testing.T) { t.Parallel() source := &model.Report{ Race: "Terrans", Votes: model.Float(0.123456789), LocalScience: []model.Science{ { Name: "science-alpha", Drive: model.Float(0.123456789), }, }, } payload, err := ReportToPayload(source) if err != nil { t.Fatalf("encode report payload: %v", err) } decoded, err := PayloadToReport(payload) if err != nil { t.Fatalf("decode report payload: %v", err) } wantVotes := model.Float(float64(float32(source.Votes.F()))) if decoded.Votes != wantVotes { t.Fatalf("unexpected votes value: got=%v want=%v", decoded.Votes, wantVotes) } wantDrive := model.Float(float64(float32(source.LocalScience[0].Drive.F()))) if decoded.LocalScience[0].Drive != wantDrive { t.Fatalf("unexpected drive value: got=%v want=%v", decoded.LocalScience[0].Drive, wantDrive) } if decoded.Votes == source.Votes { t.Fatal("expected quantization for votes value") } } func sampleReport() *model.Report { originA := uint(11) originB := uint(17) rangeA := model.Float(6.5) rangeB := model.Float(3.5) fleetName := "Fleet-1" return &model.Report{ Version: 2, Turn: 7, Width: 64, Height: 48, PlanetCount: 21, Race: "Terrans", RaceID: uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), Votes: model.Float(7.5), VoteFor: "Martians", Player: []model.Player{ { ID: uuid.MustParse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"), Name: "Terrans", Drive: model.Float(1.5), Weapons: model.Float(2.0), Shields: model.Float(2.5), Cargo: model.Float(3.0), Population: model.Float(120.0), Industry: model.Float(90.0), Planets: 5, Relation: "-", Votes: model.Float(7.5), Extinct: false, }, }, LocalScience: []model.Science{ {Name: "fusion", Drive: model.Float(1.25), Weapons: model.Float(1.5), Shields: model.Float(1.75), Cargo: model.Float(2.0)}, }, OtherScience: []model.OtherScience{ {Race: "Martians", Science: model.Science{Name: "warp", Drive: model.Float(2.0), Weapons: model.Float(2.25), Shields: model.Float(2.5), Cargo: model.Float(2.75)}}, }, LocalShipClass: []model.ShipClass{ {Name: "frigate", Drive: model.Float(1.5), Armament: 4, Weapons: model.Float(2.0), Shields: model.Float(2.5), Cargo: model.Float(3.0), Mass: model.Float(9.5)}, }, OtherShipClass: []model.OthersShipClass{ {Race: "Martians", ShipClass: model.ShipClass{Name: "destroyer", Drive: model.Float(1.75), Armament: 6, Weapons: model.Float(2.25), Shields: model.Float(2.75), Cargo: model.Float(3.25), Mass: model.Float(10.5)}}, }, Battle: []model.BattleSummary{ { ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"), Planet: 4, Shots: 17, }, { ID: uuid.MustParse("22222222-2222-2222-2222-222222222222"), Planet: 11, Shots: 3, }, }, Bombing: []*model.Bombing{ { PlanetOwnedID: uuid.MustParse("cccccccc-cccc-cccc-cccc-cccccccccccc"), Number: 9, Planet: "Nova", Owner: "Terrans", Attacker: "Martians", Production: "SHIP", Industry: model.Float(10.5), Population: model.Float(8.5), Colonists: model.Float(7.5), Capital: model.Float(6.5), Material: model.Float(5.5), AttackPower: model.Float(4.5), Wiped: false, }, }, IncomingGroup: []model.IncomingGroup{ {Origin: 1, Destination: 2, Distance: model.Float(10.0), Speed: model.Float(2.0), Mass: model.Float(20.0)}, }, LocalPlanet: []model.LocalPlanet{ { UninhabitedPlanet: model.UninhabitedPlanet{ UnidentifiedPlanet: model.UnidentifiedPlanet{X: model.Float(1.0), Y: model.Float(2.0), Number: 3}, Size: model.Float(50.0), Name: "Terra", Resources: model.Float(7.0), Capital: model.Float(8.0), Material: model.Float(9.0), }, Industry: model.Float(10.0), Population: model.Float(11.0), Colonists: model.Float(12.0), Production: "SHIP", FreeIndustry: model.Float(13.0), }, }, ShipProduction: []model.ShipProduction{ {Planet: 3, Class: "frigate", Cost: model.Float(4.0), ProdUsed: model.Float(2.0), Percent: model.Float(50.0), Free: model.Float(6.0)}, }, Route: []model.Route{ {Planet: 3, Route: map[uint]string{9: "MAT", 2: "CAP", 5: "EMP"}}, }, OtherPlanet: []model.OtherPlanet{ { Owner: "Martians", LocalPlanet: model.LocalPlanet{ UninhabitedPlanet: model.UninhabitedPlanet{ UnidentifiedPlanet: model.UnidentifiedPlanet{X: model.Float(4.0), Y: model.Float(5.0), Number: 6}, Size: model.Float(40.0), Name: "Ares", Resources: model.Float(6.0), Capital: model.Float(7.0), Material: model.Float(8.0), }, Industry: model.Float(9.0), Population: model.Float(10.0), Colonists: model.Float(11.0), Production: "MAT", FreeIndustry: model.Float(12.0), }, }, }, UninhabitedPlanet: []model.UninhabitedPlanet{ {UnidentifiedPlanet: model.UnidentifiedPlanet{X: model.Float(6.0), Y: model.Float(7.0), Number: 8}, Size: model.Float(30.0), Name: "Nadir", Resources: model.Float(5.0), Capital: model.Float(4.0), Material: model.Float(3.0)}, }, UnidentifiedPlanet: []model.UnidentifiedPlanet{ {X: model.Float(8.0), Y: model.Float(9.0), Number: 10}, }, LocalFleet: []model.LocalFleet{ {Name: "Fleet-1", Groups: 2, Destination: 4, Origin: &originA, Range: &rangeA, Speed: model.Float(2.0), State: "moving"}, }, LocalGroup: []model.LocalGroup{ { OtherGroup: model.OtherGroup{ Number: 1, Class: "frigate", Tech: map[string]model.Float{"WEAPONS": model.Float(2.0), "DRIVE": model.Float(1.5), "SHIELDS": model.Float(1.75)}, Cargo: "MAT", Load: model.Float(4.0), Destination: 4, Origin: &originB, Range: &rangeB, Speed: model.Float(2.5), Mass: model.Float(12.0), }, ID: uuid.MustParse("33333333-3333-3333-3333-333333333333"), State: "in_orbit", Fleet: &fleetName, }, }, OtherGroup: []model.OtherGroup{ {Number: 2, Class: "scout", Tech: map[string]model.Float{"CARGO": model.Float(1.25), "DRIVE": model.Float(1.75)}, Cargo: "CAP", Load: model.Float(3.5), Destination: 5, Speed: model.Float(2.25), Mass: model.Float(8.5)}, }, UnidentifiedGroup: []model.UnidentifiedGroup{ {X: model.Float(10.0), Y: model.Float(11.0)}, }, OnPlanetGroupCache: map[uint][]int{ 1: {2, 3}, }, InSpaceGroupRangeCache: map[int]map[uint]float64{ 1: {4: 12.5}, }, } } func reportWireClone(t *testing.T, source *model.Report) *model.Report { t.Helper() data, err := source.MarshalBinary() if err != nil { t.Fatalf("marshal source report: %v", err) } result := new(model.Report) if err := result.UnmarshalBinary(data); err != nil { t.Fatalf("unmarshal source report clone: %v", err) } return result } func buildReportPayload(build func(*flatbuffers.Builder) flatbuffers.UOffsetT) []byte { builder := flatbuffers.NewBuilder(256) offset := build(builder) fbs.FinishReportBuffer(builder, offset) return builder.FinishedBytes() }