flatbuffers & transcoders
This commit is contained in:
@@ -0,0 +1,355 @@
|
||||
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 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: []uuid.UUID{
|
||||
uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
||||
uuid.MustParse("22222222-2222-2222-2222-222222222222"),
|
||||
},
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user