package legacyreport import ( "os" "path/filepath" "strings" "testing" "galaxy/model/report" ) // TestParseHeaderAndSize covers the standalone single-line preamble. func TestParseHeaderAndSize(t *testing.T) { in := strings.Join([]string{ " KnightErrants Report for Galaxy PLUS dg283 Turn 39 Thu Jul 06 09:01:16 2000", "", " Size: 800 Planets: 700 Players: 91", "", }, "\n") rep, err := Parse(strings.NewReader(in)) if err != nil { t.Fatalf("Parse: %v", err) } if got, want := rep.Race, "KnightErrants"; got != want { t.Errorf("Race = %q, want %q", got, want) } if got, want := rep.Turn, uint(39); got != want { t.Errorf("Turn = %d, want %d", got, want) } if got, want := rep.Width, uint32(800); got != want { t.Errorf("Width = %d, want %d", got, want) } if got, want := rep.Height, uint32(800); got != want { t.Errorf("Height = %d, want %d", got, want) } if got, want := rep.PlanetCount, uint32(700); got != want { t.Errorf("PlanetCount = %d, want %d", got, want) } } // TestParseStatusOfPlayers exercises the alive / extinct distinction // that drives the races view. func TestParseStatusOfPlayers(t *testing.T) { in := strings.Join([]string{ "Race Report for Galaxy PLUS Turn 1", "", "Status of Players (total 10.00 votes)", "", "N D W S C P I # R V", "Alpha 4.51 2.24 1.80 1.00 100.00 80.00 3 War 3.00", "Bravo 9.03 5.62 2.16 1.53 200.00 150.00 5 Peace 5.00", "Gone_RIP 1.00 1.00 1.00 1.00 0.00 0.00 0 War 0.00", "", }, "\n") rep, err := Parse(strings.NewReader(in)) if err != nil { t.Fatalf("Parse: %v", err) } if got, want := len(rep.Player), 3; got != want { t.Fatalf("len(Player) = %d, want %d", got, want) } alpha := rep.Player[0] if alpha.Name != "Alpha" || alpha.Extinct { t.Errorf("Alpha = %+v, want Name=Alpha extinct=false", alpha) } if alpha.Planets != 3 || alpha.Relation != "War" { t.Errorf("Alpha planets/relation = %d/%q, want 3/War", alpha.Planets, alpha.Relation) } if got, want := float64(alpha.Drive), 4.51; got != want { t.Errorf("Alpha.Drive = %v, want %v", got, want) } gone := rep.Player[2] if gone.Name != "Gone" || !gone.Extinct { t.Errorf("Gone = %+v, want Name=Gone extinct=true (suffix _RIP stripped)", gone) } } func TestParseYourVote(t *testing.T) { in := strings.Join([]string{ "Race Report for Galaxy PLUS Turn 1", "", "Your vote:", "", "R V", "KnightErrants 16.02", "", }, "\n") rep, err := Parse(strings.NewReader(in)) if err != nil { t.Fatalf("Parse: %v", err) } if rep.VoteFor != "KnightErrants" { t.Errorf("VoteFor = %q, want KnightErrants", rep.VoteFor) } if got, want := float64(rep.Votes), 16.02; got != want { t.Errorf("Votes = %v, want %v", got, want) } } func TestParseLocalAndOtherPlanets(t *testing.T) { in := strings.Join([]string{ "Race Report for Galaxy PLUS Turn 1", "", "Your Planets", "", " # X Y N S P I R P $ M C L", " 17 171.05 700.24 Castle 1000.00 1000.00 1000.00 10.00 Drive_Research 0.00 0.68 88.78 1000.00", " 87 169.59 694.49 North 500.00 500.00 500.00 10.00 Capital 0.00 0.52 35.76 500.00", "", "Monstrai Planets", "", " # X Y N S P I R P $ M C L", " 12 303.84 579.23 Skarabei 500.00 500.00 500.00 10.00 Capital 0.00 70.99 20.03 341.78", "", }, "\n") rep, err := Parse(strings.NewReader(in)) if err != nil { t.Fatalf("Parse: %v", err) } if got, want := len(rep.LocalPlanet), 2; got != want { t.Fatalf("len(LocalPlanet) = %d, want %d", got, want) } castle := rep.LocalPlanet[0] if castle.Number != 17 || castle.Name != "Castle" { t.Errorf("Castle = (%d, %q), want (17, Castle)", castle.Number, castle.Name) } if castle.Production != "Drive_Research" { t.Errorf("Castle.Production = %q, want Drive_Research", castle.Production) } if got, want := float64(castle.Size), 1000.0; got != want { t.Errorf("Castle.Size = %v, want %v", got, want) } if got, want := len(rep.OtherPlanet), 1; got != want { t.Fatalf("len(OtherPlanet) = %d, want %d", got, want) } skarabei := rep.OtherPlanet[0] if skarabei.Owner != "Monstrai" { t.Errorf("Skarabei.Owner = %q, want Monstrai", skarabei.Owner) } if skarabei.Number != 12 || skarabei.Name != "Skarabei" { t.Errorf("Skarabei = (%d, %q), want (12, Skarabei)", skarabei.Number, skarabei.Name) } } func TestParseUninhabitedAndUnidentified(t *testing.T) { in := strings.Join([]string{ "Race Report for Galaxy PLUS Turn 1", "", "Uninhabited Planets", "", " # X Y N S R $ M", " 9 117.87 795.21 Dw2 500.00 10.00 0.00 500.00", "", "Unidentified Planets", "", " # X Y", " 0 738.08 600.26", " 1 579.12 489.37", "", }, "\n") rep, err := Parse(strings.NewReader(in)) if err != nil { t.Fatalf("Parse: %v", err) } if got, want := len(rep.UninhabitedPlanet), 1; got != want { t.Fatalf("len(UninhabitedPlanet) = %d, want %d", got, want) } dw2 := rep.UninhabitedPlanet[0] if dw2.Number != 9 || dw2.Name != "Dw2" { t.Errorf("Dw2 = (%d, %q), want (9, Dw2)", dw2.Number, dw2.Name) } if got, want := len(rep.UnidentifiedPlanet), 2; got != want { t.Fatalf("len(UnidentifiedPlanet) = %d, want %d", got, want) } first := rep.UnidentifiedPlanet[0] if first.Number != 0 || float64(first.X) != 738.08 || float64(first.Y) != 600.26 { t.Errorf("Unidentified[0] = %+v, want (0, 738.08, 600.26)", first) } } func TestParseShipClasses(t *testing.T) { in := strings.Join([]string{ "Race Report for Galaxy PLUS Turn 1", "", "Your Ship Types", "", "N D A W S C M", "Frontier 11.37 0 0.00 0.00 1.00 12.37", "Bow105 74.77 105 1.00 19.72 1.00 148.49", "", "Monstrai Ship Types", "", "N D A W S C M", "Dragon 16.70 1 1.10 1.00 1 19.80", "", }, "\n") rep, err := Parse(strings.NewReader(in)) if err != nil { t.Fatalf("Parse: %v", err) } if got, want := len(rep.LocalShipClass), 2; got != want { t.Fatalf("len(LocalShipClass) = %d, want %d", got, want) } bow := rep.LocalShipClass[1] if bow.Name != "Bow105" || bow.Armament != 105 { t.Errorf("Bow105 name/armament = %q/%d, want Bow105/105", bow.Name, bow.Armament) } if got, want := float64(bow.Drive), 74.77; got != want { t.Errorf("Bow105.Drive = %v, want %v", got, want) } if got, want := len(rep.OtherShipClass), 1; got != want { t.Fatalf("len(OtherShipClass) = %d, want %d", got, want) } dragon := rep.OtherShipClass[0] if dragon.Race != "Monstrai" || dragon.Name != "Dragon" || dragon.Armament != 1 { t.Errorf("Dragon = (%q, %q, %d), want (Monstrai, Dragon, 1)", dragon.Race, dragon.Name, dragon.Armament) } if got, want := float64(dragon.Mass), 19.80; got != want { t.Errorf("Dragon.Mass = %v, want %v", got, want) } } // TestParseSciences covers both "Your Sciences" and " Sciences" // in one fixture. The five-column layout (N D W S C) is shared. func TestParseSciences(t *testing.T) { in := strings.Join([]string{ "Race Report for Galaxy PLUS Turn 1", "", "Your Sciences", "", "N D W S C", "_TerraForm 1 0 0 0", "BalancedMix 0.5 0.2 0.2 0.1", "", "Pahanchiks Sciences", "", "N D W S C", "_Drift 1 0 0 0", "", }, "\n") rep, err := Parse(strings.NewReader(in)) if err != nil { t.Fatalf("Parse: %v", err) } if got, want := len(rep.LocalScience), 2; got != want { t.Fatalf("len(LocalScience) = %d, want %d", got, want) } mix := rep.LocalScience[1] if mix.Name != "BalancedMix" { t.Errorf("LocalScience[1].Name = %q, want BalancedMix", mix.Name) } if float64(mix.Drive) != 0.5 || float64(mix.Cargo) != 0.1 { t.Errorf("BalancedMix (Drive, Cargo) = (%v, %v), want (0.5, 0.1)", float64(mix.Drive), float64(mix.Cargo)) } if got, want := len(rep.OtherScience), 1; got != want { t.Fatalf("len(OtherScience) = %d, want %d", got, want) } drift := rep.OtherScience[0] if drift.Race != "Pahanchiks" || drift.Name != "_Drift" { t.Errorf("OtherScience[0] = (%q, %q), want (Pahanchiks, _Drift)", drift.Race, drift.Name) } } // TestParseBombings covers a wiped row + a damaged row + the duplicate // `P` column header (population vs production string) — assertions // hit every wire field so a positional-index slip is caught. func TestParseBombings(t *testing.T) { in := strings.Join([]string{ "Race Report for Galaxy PLUS Turn 1", "", "Bombings", "", "W O # N P I P $ M C A", "Knights Ricksha 20 DW-1207 1.56 0.00 Dron 0.00 0.00 0.00 7.62 Wiped", "Knights Ricksha 332 PEHKE 500.00 258.64 Dron 184.39 0.00 6.42 331.93 Damaged", "", }, "\n") rep, err := Parse(strings.NewReader(in)) if err != nil { t.Fatalf("Parse: %v", err) } if got, want := len(rep.Bombing), 2; got != want { t.Fatalf("len(Bombing) = %d, want %d", got, want) } wiped := rep.Bombing[0] if !wiped.Wiped { t.Errorf("Bombing[0].Wiped = false, want true") } if wiped.Attacker != "Knights" || wiped.Owner != "Ricksha" || wiped.Number != 20 { t.Errorf("Bombing[0] head = (%q, %q, %d), want (Knights, Ricksha, 20)", wiped.Attacker, wiped.Owner, wiped.Number) } if wiped.Planet != "DW-1207" || wiped.Production != "Dron" { t.Errorf("Bombing[0] (planet, production) = (%q, %q), want (DW-1207, Dron)", wiped.Planet, wiped.Production) } if float64(wiped.AttackPower) != 7.62 { t.Errorf("Bombing[0].AttackPower = %v, want 7.62", float64(wiped.AttackPower)) } damaged := rep.Bombing[1] if damaged.Wiped { t.Errorf("Bombing[1].Wiped = true, want false (Damaged)") } if float64(damaged.Capital) != 184.39 || float64(damaged.Colonists) != 6.42 { t.Errorf("Bombing[1] (capital, colonists) = (%v, %v), want (184.39, 6.42)", float64(damaged.Capital), float64(damaged.Colonists)) } } // TestParseShipsInProduction covers the prod_used derivation through // [calc.ShipBuildCost]. The producing planet is mounted first with a // non-zero material stockpile so the farming term contributes a // non-trivial slice of totalCost; the expected prod_used number is // derived from totalCost * percent with the same formula the parser // uses. func TestParseShipsInProduction(t *testing.T) { // Planet: Material=0.68, Resources=10.00. // Ship: cost=990.10 -> shipMass=99.01. // totalCost = ShipProductionCost(99.01) + max(0, 99.01-0.68)/10 // = 990.10 + 9.833 // = 999.933 // prod_used = 999.933 * 0.07 (percent) ≈ 69.99531 in := strings.Join([]string{ "Race Report for Galaxy PLUS Turn 1", "", "Your Planets", "", " # X Y N S P I R P $ M C L", " 17 171.05 700.24 Castle 1000.00 1000.00 1000.00 10.00 CombatFlame 0.00 0.68 88.78 1000.00", "", "Ships In Production", "", " # N S C P L", " 17 Castle CombatFlame 990.10 0.07 1000.00", "", }, "\n") rep, err := Parse(strings.NewReader(in)) if err != nil { t.Fatalf("Parse: %v", err) } if got, want := len(rep.ShipProduction), 1; got != want { t.Fatalf("len(ShipProduction) = %d, want %d", got, want) } sp := rep.ShipProduction[0] if sp.Planet != 17 || sp.Class != "CombatFlame" { t.Errorf("ShipProduction[0] = (planet=%d, class=%q), want (17, CombatFlame)", sp.Planet, sp.Class) } if got := float64(sp.Cost); got != 990.10 { t.Errorf("ShipProduction[0].Cost = %v, want 990.10", got) } if got := float64(sp.Percent); got != 0.07 { t.Errorf("ShipProduction[0].Percent = %v, want 0.07", got) } if got := float64(sp.Free); got != 1000.0 { t.Errorf("ShipProduction[0].Free = %v, want 1000", got) } wantProdUsed := 69.995 if got := float64(sp.ProdUsed); got < wantProdUsed-0.01 || got > wantProdUsed+0.01 { t.Errorf("ShipProduction[0].ProdUsed = %v, want ~%v (totalCost * percent)", got, wantProdUsed) } } // TestParseShipsInProductionDropsUnknownPlanet exercises the safety // net: a ships-in-production row referencing a planet not seen in // "Your Planets" is dropped, because the prod_used derivation needs // the planet's material and resources. func TestParseShipsInProductionDropsUnknownPlanet(t *testing.T) { in := strings.Join([]string{ "Race Report for Galaxy PLUS Turn 1", "", "Ships In Production", "", " # N S C P L", " 99 Lost Frigate 100.00 0.05 500.00", "", }, "\n") rep, err := Parse(strings.NewReader(in)) if err != nil { t.Fatalf("Parse: %v", err) } if got, want := len(rep.ShipProduction), 0; got != want { t.Errorf("len(ShipProduction) = %d, want %d (planet #99 missing → drop)", got, want) } } // TestParseSkipsBattles covers the only remaining legacy section the // parser ignores: "Battle at ..." headers and the following "Battle // Protocol" block. Bombings, Ships In Production, and the per-race // Sciences / Ship Types blocks now flow through real parsers; the // dedicated section tests below cover them. func TestParseSkipsBattles(t *testing.T) { in := strings.Join([]string{ "Race Report for Galaxy PLUS Turn 1", "", "Battle at (#7) B-007", "", "# T D W S C T Q L", "1 PeaceShip 4 0 0 0 - 0 1 Out_Battle", "", "Battle Protocol", "", "Foo fires on Bar : Destroyed", "", "Your Planets", "", " # X Y N S P I R P $ M C L", " 17 171.05 700.24 Castle 1000.00 1000.00 1000.00 10.00 Capital 0.00 0.68 88.78 1000.00", "", }, "\n") rep, err := Parse(strings.NewReader(in)) if err != nil { t.Fatalf("Parse: %v", err) } if got, want := len(rep.LocalPlanet), 1; got != want { t.Fatalf("len(LocalPlanet) = %d, want %d (battle rows must not leak in)", got, want) } if got, want := len(rep.Battle), 0; got != want { t.Errorf("len(Battle) = %d, want %d (legacy parser does not synthesise battle UUIDs)", got, want) } } // TestParseYourGroups exercises the local-group section. Two rows // cover the on-planet ("In_Orbit", origin "-") and in-space ("In_Space", // origin name + range) variants, plus a cargo-loaded row to assert the // load-type / load-quantity columns are wired through. A planet // table is mounted upfront so destination/origin name resolution // has something to bind against. func TestParseYourGroups(t *testing.T) { in := strings.Join([]string{ "Race Report for Galaxy PLUS Turn 1", "", "Your Planets", "", " # X Y N S P I R P $ M C L", " 17 171.05 700.24 Castle 1000.00 1000.00 1000.00 10.00 Capital 0.00 0.68 88.78 1000.00", " 87 169.59 694.49 North 500.00 500.00 500.00 10.00 Capital 0.00 0.52 35.76 500.00", "", "Your Groups", "", " G # T D W S C T Q D F R P M L", " 0 2 Frontier 5.05 0.00 0.00 1.0 - 0 Castle - - 92.84 12.37 - In_Orbit", " 1 1 Bow105 11.19 4.76 7.09 1.0 COL 1 Castle - - 111.9 149.54 - In_Orbit", " 2 1 Tormoz 11.19 0.00 0.00 1.0 CAP 4 North Castle 7.5 60.66 49.50 - In_Space", "", }, "\n") rep, err := Parse(strings.NewReader(in)) if err != nil { t.Fatalf("Parse: %v", err) } if got, want := len(rep.LocalGroup), 3; got != want { t.Fatalf("len(LocalGroup) = %d, want %d", got, want) } first := rep.LocalGroup[0] if first.Number != 2 || first.Class != "Frontier" { t.Errorf("first group = (%d, %q), want (2, Frontier)", first.Number, first.Class) } if got, want := float64(first.Tech["drive"]), 5.05; got != want { t.Errorf("first.Tech[drive] = %v, want %v", got, want) } if first.Destination != 17 { t.Errorf("first.Destination = %d, want 17 (Castle resolved)", first.Destination) } if first.Origin != nil || first.Range != nil { t.Errorf("first.{Origin,Range} = (%v, %v), want both nil for In_Orbit", first.Origin, first.Range) } if first.State != "In_Orbit" { t.Errorf("first.State = %q, want In_Orbit", first.State) } if first.Fleet != nil { t.Errorf("first.Fleet = %v, want nil for `-`", first.Fleet) } loaded := rep.LocalGroup[1] if loaded.Cargo != "COL" || float64(loaded.Load) != 1.0 { t.Errorf("loaded cargo/load = (%q, %v), want (COL, 1.0)", loaded.Cargo, float64(loaded.Load)) } flying := rep.LocalGroup[2] if flying.State != "In_Space" { t.Errorf("flying.State = %q, want In_Space", flying.State) } if flying.Origin == nil || *flying.Origin != 17 { t.Errorf("flying.Origin = %v, want 17 (Castle)", flying.Origin) } if flying.Range == nil || float64(*flying.Range) != 7.5 { t.Errorf("flying.Range = %v, want 7.5", flying.Range) } if flying.Destination != 87 { t.Errorf("flying.Destination = %d, want 87 (North)", flying.Destination) } } func TestParseYourFleets(t *testing.T) { in := strings.Join([]string{ "Race Report for Galaxy PLUS Turn 1", "", "Your Planets", "", " # X Y N S P I R P $ M C L", " 17 171.05 700.24 Castle 1000.00 1000.00 1000.00 10.00 Capital 0.00 0.68 88.78 1000.00", " 87 169.59 694.49 North 500.00 500.00 500.00 10.00 Capital 0.00 0.52 35.76 500.00", "", "Your Fleets", "", " # N G D F R P", " 0 Fast 3 Castle - - 45 In_Orbit", " 1 Far 2 North Castle 4.50 20 In_Space", "", }, "\n") rep, err := Parse(strings.NewReader(in)) if err != nil { t.Fatalf("Parse: %v", err) } if got, want := len(rep.LocalFleet), 2; got != want { t.Fatalf("len(LocalFleet) = %d, want %d", got, want) } fast := rep.LocalFleet[0] if fast.Name != "Fast" || fast.Groups != 3 || fast.Destination != 17 { t.Errorf("fast = %+v, want Name=Fast Groups=3 Destination=17", fast) } if fast.State != "In_Orbit" { t.Errorf("fast.State = %q, want In_Orbit", fast.State) } if fast.Origin != nil || fast.Range != nil { t.Errorf("fast.{Origin,Range} = (%v, %v), want both nil", fast.Origin, fast.Range) } far := rep.LocalFleet[1] if far.Origin == nil || *far.Origin != 17 { t.Errorf("far.Origin = %v, want 17 (Castle)", far.Origin) } if far.Range == nil || float64(*far.Range) != 4.5 { t.Errorf("far.Range = %v, want 4.5", far.Range) } } func TestParseIncomingGroups(t *testing.T) { // Origin is a `#NN` by-id reference; destination resolves // against the local planet table that was parsed earlier in // this synthetic file. The order is intentionally swapped — in // real legacy reports "Incoming Groups" can land before // "Your Planets", which is why the parser buffers rows for // post-processing. in := strings.Join([]string{ "Race Report for Galaxy PLUS Turn 1", "", "Incoming Groups", "", "O D R S M", "#98 Castle 136.16 190 1", "North Castle 42.12 99 2", "", "Your Planets", "", " # X Y N S P I R P $ M C L", " 17 171.05 700.24 Castle 1000.00 1000.00 1000.00 10.00 Capital 0.00 0.68 88.78 1000.00", " 87 169.59 694.49 North 500.00 500.00 500.00 10.00 Capital 0.00 0.52 35.76 500.00", "", }, "\n") rep, err := Parse(strings.NewReader(in)) if err != nil { t.Fatalf("Parse: %v", err) } if got, want := len(rep.IncomingGroup), 2; got != want { t.Fatalf("len(IncomingGroup) = %d, want %d", got, want) } a := rep.IncomingGroup[0] if a.Origin != 98 || a.Destination != 17 { t.Errorf("a (Origin, Destination) = (%d, %d), want (98, 17)", a.Origin, a.Destination) } if float64(a.Distance) != 136.16 || float64(a.Speed) != 190 { t.Errorf("a (Distance, Speed) = (%v, %v), want (136.16, 190)", float64(a.Distance), float64(a.Speed)) } b := rep.IncomingGroup[1] if b.Origin != 87 || b.Destination != 17 { t.Errorf("b (Origin, Destination) = (%d, %d), want (87, 17)", b.Origin, b.Destination) } } // --- smoke tests ----------------------------------------------------- type smokeWant struct { race string turn uint mapW, mapH, planetCount uint32 voteFor string votes float64 players, extinct, local, other int uninhabited, unidentified, shipClasses int localGroups, localFleets, incomingGroups int localScience, otherScience, otherShipClass int bombings, shipProductions int } func runSmoke(t *testing.T, path string, want smokeWant) { t.Helper() rep, err := parseFile(t, path) if err != nil { if os.IsNotExist(err) { t.Skipf("legacy report fixture missing: %s", path) } t.Fatalf("Parse %s: %v", path, err) } if rep.Race != want.race || rep.Turn != want.turn { t.Errorf("(race, turn) = (%q, %d), want (%q, %d)", rep.Race, rep.Turn, want.race, want.turn) } if rep.Width != want.mapW || rep.Height != want.mapH || rep.PlanetCount != want.planetCount { t.Errorf("(W, H, planets) = (%d, %d, %d), want (%d, %d, %d)", rep.Width, rep.Height, rep.PlanetCount, want.mapW, want.mapH, want.planetCount) } if want.voteFor != "" { if rep.VoteFor != want.voteFor || float64(rep.Votes) != want.votes { t.Errorf("(voteFor, votes) = (%q, %v), want (%q, %v)", rep.VoteFor, float64(rep.Votes), want.voteFor, want.votes) } } extinct := 0 for _, pl := range rep.Player { if pl.Extinct { extinct++ } } checks := []struct { name string got int want int }{ {"Player", len(rep.Player), want.players}, {"extinct", extinct, want.extinct}, {"LocalPlanet", len(rep.LocalPlanet), want.local}, {"OtherPlanet", len(rep.OtherPlanet), want.other}, {"UninhabitedPlanet", len(rep.UninhabitedPlanet), want.uninhabited}, {"UnidentifiedPlanet", len(rep.UnidentifiedPlanet), want.unidentified}, {"LocalShipClass", len(rep.LocalShipClass), want.shipClasses}, {"LocalGroup", len(rep.LocalGroup), want.localGroups}, {"LocalFleet", len(rep.LocalFleet), want.localFleets}, {"IncomingGroup", len(rep.IncomingGroup), want.incomingGroups}, {"LocalScience", len(rep.LocalScience), want.localScience}, {"OtherScience", len(rep.OtherScience), want.otherScience}, {"OtherShipClass", len(rep.OtherShipClass), want.otherShipClass}, {"Bombing", len(rep.Bombing), want.bombings}, {"ShipProduction", len(rep.ShipProduction), want.shipProductions}, } for _, c := range checks { if c.got != c.want { t.Errorf("%s = %d, want %d", c.name, c.got, c.want) } } } // TestParseDgKNNTS039 is a smoke test: the parser must produce // stable top-line counts from the real dg/KNNTS039.REP fixture. // Field-level fidelity is asserted in the unit tests above; this // test catches regressions where a section-classifier change // silently drops half the data. func TestParseDgKNNTS039(t *testing.T) { runSmoke(t, "../reports/dg/KNNTS039.REP", smokeWant{ race: "KnightErrants", turn: 39, mapW: 800, mapH: 800, planetCount: 700, voteFor: "KnightErrants", votes: 16.02, players: 91, extinct: 49, local: 22, other: 89, uninhabited: 17, unidentified: 572, shipClasses: 24, localGroups: 171, localFleets: 0, incomingGroups: 0, localScience: 1, otherScience: 1, otherShipClass: 170, bombings: 16, shipProductions: 6, }) } func TestParseDgKNNTS040(t *testing.T) { runSmoke(t, "../reports/dg/KNNTS040.REP", smokeWant{ race: "KnightErrants", turn: 40, mapW: 800, mapH: 800, planetCount: 700, players: 91, extinct: 49, local: 22, other: 93, uninhabited: 27, unidentified: 558, shipClasses: 34, localGroups: 207, localFleets: 0, incomingGroups: 0, localScience: 1, otherScience: 1, otherShipClass: 160, bombings: 24, shipProductions: 16, }) } // TestParseDgKNNTS041 covers a turn with active "Incoming Groups" // entries (12 rows) appearing before the "Your Planets" table — // exercises the deferred name-resolution path in [parser.finish]. func TestParseDgKNNTS041(t *testing.T) { runSmoke(t, "../reports/dg/KNNTS041.REP", smokeWant{ race: "KnightErrants", turn: 41, mapW: 800, mapH: 800, planetCount: 700, players: 91, extinct: 50, local: 29, other: 103, uninhabited: 23, unidentified: 545, shipClasses: 36, localGroups: 285, localFleets: 0, incomingGroups: 12, localScience: 1, otherScience: 1, otherShipClass: 218, bombings: 12, shipProductions: 22, }) } // TestParseGplus40 exercises the gplus engine variant (tabs in // section headers, pseudo-cyrillic ASCII names) and a single fleet. // gplus also sneaks "Incoming Groups" between sections. func TestParseGplus40(t *testing.T) { runSmoke(t, "../reports/gplus/40.REP", smokeWant{ race: "MbI", turn: 40, mapW: 350, mapH: 350, planetCount: 300, players: 26, extinct: 0, local: 26, other: 116, uninhabited: 7, unidentified: 151, shipClasses: 56, localGroups: 255, localFleets: 1, incomingGroups: 10, localScience: 0, otherScience: 0, otherShipClass: 183, bombings: 4, shipProductions: 8, }) } // TestParseDgKiller031 exercises the Killer engine variant which // ships "Your Fleets" + "Your Groups" with the Fl1/F2 fleet // membership shape (no "Incoming Groups" this turn). func TestParseDgKiller031(t *testing.T) { runSmoke(t, "../reports/dg/Killer031.rep", smokeWant{ race: "Killer", turn: 31, mapW: 250, mapH: 250, planetCount: 175, players: 25, extinct: 12, local: 18, other: 127, uninhabited: 20, unidentified: 10, shipClasses: 11, localGroups: 175, localFleets: 2, incomingGroups: 0, localScience: 0, otherScience: 0, otherShipClass: 161, bombings: 18, shipProductions: 0, }) } // TestParseDgTancordia037 is the richest smoke fixture: it carries // 311 local groups across 30 fleets, two incoming groups, and the // "Incoming Groups" section appears before "Your Planets" (so the // deferred name resolution is exercised in production conditions). func TestParseDgTancordia037(t *testing.T) { runSmoke(t, "../reports/dg/Tancordia037.rep", smokeWant{ race: "Tancordia", turn: 37, mapW: 210, mapH: 210, planetCount: 140, players: 18, extinct: 7, local: 23, other: 62, uninhabited: 26, unidentified: 29, shipClasses: 40, localGroups: 311, localFleets: 30, incomingGroups: 2, localScience: 1, otherScience: 1, otherShipClass: 123, bombings: 22, shipProductions: 20, }) } func parseFile(t *testing.T, rel string) (report.Report, error) { t.Helper() abs, err := filepath.Abs(rel) if err != nil { return report.Report{}, err } f, err := os.Open(abs) if err != nil { return report.Report{}, err } defer func() { _ = f.Close() }() return Parse(f) }