Aetheria/tests/Aetheria.Shared.Tests/GenericDesignsCommandTests.cs
Meta Vens 205c9f5205 Briefs, comments and tests name current gear; generic-designs lands only its own records and rewrites the last flavour and brand design texts
Scenario Briefs (Crossing, Duel, FusedRounds, LauncherAngles, StarvedReactor, WeaponFeel), scenario and test comments and test locals say the design names (Lightning Projector, Light Laser, Cell Blaster, Guided Missile Launcher, Swarm Rocket Pod, Muon Fusion Reactor, Cluster Cannon, Charged Scatter Blaster, Rugged Drive, Racing Drive) instead of the retired brand names; Crossing.PlightRange is ProjectorRange.

generic-designs: the batch carries ExpectUnchanged so every record it does not author keeps its bytes (the plain commit re-encoded four records, dropping SensorData key 7). DesignTexts rewrites Large Drive, Tractor Beam, Smoke Proximity, Smoke Delayed, Smoke Refused and Smoke Refused Charge as tech-node text; the Tractor Beam's old line moves to its product Space Tractor. smoke-rounds' authored text names Light Laser and Charged Scatter Blaster to match.

Tests: NoScenarioBriefNamesRetiredGear, TheCapacitorDesignCarriesItsRoleMap, NoDesignTextNamesAProduct, and a command test that widens every payload and checks apply changes only the three records it authors and restores the rewritten texts.

Not built or run at this commit; GameData/Aetheria.cc is regenerated in the next commit, so NoDesignTextNamesAProduct and the command test fail here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:34:08 +02:00

239 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using GameCult.Caching;
using Xunit;
// AetherDb's generic-designs command (aetheria-release, cut generic-design-names), run on a scratch copy of the shipped
// catalog, which already carries the applied result: each case first puts a few records back to their old shape.
public sealed class GenericDesignsCommandTests : IDisposable
{
private readonly string _root = Path.Combine(Path.GetTempPath(), "generic-designs-command-" + Guid.NewGuid().ToString("N"));
private string CatalogPath => Path.Combine(_root, "GameData", "Aetheria.cc");
public GenericDesignsCommandTests()
{
Directory.CreateDirectory(Path.Combine(_root, "GameData"));
Directory.CreateDirectory(Path.Combine(_root, "Assets", "Resources"));
var shipped = AetherDb.FindRoot();
File.Copy(Path.Combine(shipped, "GameData", "Aetheria.cc"), CatalogPath);
File.Copy(Path.Combine(shipped, "Assets", "Resources", "Settings.asset"), Path.Combine(_root, "Assets", "Resources", "Settings.asset"));
}
public void Dispose() => Directory.Delete(_root, true);
private void Edit(Action<AetherDb> edit)
{
var db = AetherDb.Open(catalogWritable: true, root: _root);
try { edit(db); }
finally { db.Cache.Dispose(); }
}
private T Read<T>(Func<AetherDb, T> read)
{
var db = AetherDb.Open(root: _root);
try { return read(db); }
finally { db.Cache.Dispose(); }
}
private static void Save(AetherDb db, EquippableItemData design) =>
db.Cache.Commit(batch => batch.Upsert(design.GetType(), design, db.Cache.RefOf(design).Key));
private static void Save(AetherDb db, FactionProductData product) =>
db.Cache.Commit(batch => batch.Upsert(typeof(FactionProductData), product, db.Cache.RefOf(product).Key));
private static EquippableItemData DesignNamed(AetherDb db, string name) =>
db.Cache.GetAll<EquippableItemData>().Single(d => d.Name == name);
private static FactionProductData ProductNamed(AetherDb db, string name) =>
db.Cache.GetAll<FactionProductData>().Single(p => p.Name == name);
[Fact]
public void An_applied_catalog_is_left_alone_by_a_dry_run_and_by_apply()
{
var before = File.ReadAllBytes(CatalogPath);
Assert.Equal(0, Program.GenericDesigns(apply: false, root: _root));
Assert.Equal(before, File.ReadAllBytes(CatalogPath));
Assert.Equal(0, Program.GenericDesigns(apply: true, root: _root));
Assert.Equal(before, File.ReadAllBytes(CatalogPath));
}
[Fact]
public void A_dry_run_writes_nothing_and_apply_restores_both_records()
{
var (designText, productText, brandedDesignText, rowText) = Read(db => (DesignNamed(db, "Light Machine Gun").Description,
ProductNamed(db, "Hammerfall").Description, DesignNamed(db, "Autocannon").Description, ProductNamed(db, "Skiron").Description));
Edit(db =>
{
var design = DesignNamed(db, "Light Machine Gun");
design.Name = "ClearPath";
design.Description = "Defend your precious cargo.";
Save(db, design);
var product = ProductNamed(db, "Hammerfall");
product.Name = "Autocannon";
product.Description = "";
Save(db, product);
var branded = DesignNamed(db, "Autocannon");
branded.Description = "";
Save(db, branded);
var row = ProductNamed(db, "Skiron");
row.Description = "";
Save(db, row);
});
var reverted = File.ReadAllBytes(CatalogPath);
Assert.Equal(0, Program.GenericDesigns(apply: false, root: _root));
Assert.Equal(reverted, File.ReadAllBytes(CatalogPath));
Assert.Equal(0, Program.GenericDesigns(apply: true, root: _root));
Read(db =>
{
Assert.Equal(designText, DesignNamed(db, "Light Machine Gun").Description);
Assert.DoesNotContain(db.Cache.GetAll<EquippableItemData>(), d => d.Name == "ClearPath");
Assert.Equal(productText, ProductNamed(db, "Hammerfall").Description);
Assert.Equal(brandedDesignText, DesignNamed(db, "Autocannon").Description);
Assert.Equal(rowText, ProductNamed(db, "Skiron").Description);
Assert.DoesNotContain(db.Cache.GetAll<FactionProductData>(), p => p.Name == "Autocannon" && db.Cache.Get(p.Manufacturer)?.ShortName == "Zhestokost");
return 0;
});
}
// Re-spell every record's payload in a longer, equally valid MessagePack form (an array16 header for the fixarray one). The
// catalog then holds bytes no re-encode through the current types would reproduce, so a write that re-encodes records it did
// not author shows as changed bytes; a write that lands onto the file as it is leaves them alone.
private int WidenEveryPayload()
{
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(CatalogPath));
var widened = 0;
foreach (var record in snapshot.Records)
{
var payload = record.Payload;
if (payload.Length == 0 || (payload[0] & 0xF0) != 0x90) continue;
record.Payload = new byte[] { 0xDC, 0x00, (byte)(payload[0] & 0x0F) }.Concat(payload.Skip(1)).ToArray();
widened++;
}
File.WriteAllBytes(CatalogPath, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot));
return widened;
}
private Dictionary<string, CultPersistedRecord> Records() =>
CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(CatalogPath)).Records.ToDictionary(r => r.Key, StringComparer.Ordinal);
[Fact]
public void Apply_writes_only_the_records_it_authors_and_restores_the_rewritten_design_texts()
{
var (largeDriveText, tractorText, smokeText, tractorProductText) = Read(db => (DesignNamed(db, "Large Drive").Description, DesignNamed(db, "Tractor Beam").Description,
DesignNamed(db, "Smoke Proximity").Description, ProductNamed(db, "Space Tractor").Description));
Edit(db =>
{
var drive = DesignNamed(db, "Large Drive");
drive.Description = "Keeps you going and going and going...";
Save(db, drive);
var smoke = DesignNamed(db, "Smoke Proximity");
smoke.Description = "A Spectra that bursts by proximity, blast 8. For scenarios; no one builds it.";
Save(db, smoke);
var tractor = ProductNamed(db, "Space Tractor");
tractor.Description = "It's a tractor. In space.";
Save(db, tractor);
});
Assert.True(WidenEveryPayload() > 100, "the scratch catalog should hold many records in the widened spelling");
var before = Records();
Assert.Equal(0, Program.GenericDesigns(apply: true, root: _root));
var after = Records();
Assert.Equal(before.Count, after.Count);
var changed = before.Keys.Where(key => !before[key].Payload.AsSpan().SequenceEqual(after[key].Payload)).ToArray();
Assert.Equal(3, changed.Length);
foreach (var (key, record) in before.Where(pair => !changed.Contains(pair.Key)))
Assert.True(record.Payload.AsSpan().SequenceEqual(after[key].Payload), "a record the command did not author has changed bytes");
Read(db =>
{
Assert.Equal(largeDriveText, DesignNamed(db, "Large Drive").Description);
Assert.Equal(tractorText, DesignNamed(db, "Tractor Beam").Description);
Assert.Equal(smokeText, DesignNamed(db, "Smoke Proximity").Description);
Assert.Equal(tractorProductText, ProductNamed(db, "Space Tractor").Description);
return 0;
});
}
[Fact]
public void A_generic_name_held_by_another_design_is_refused_and_nothing_is_written()
{
Edit(db =>
{
var cargo = DesignNamed(db, "Cargo Bay");
cargo.Name = "Store-All Plus";
Save(db, cargo);
var flak = DesignNamed(db, "Flak Gun");
flak.Name = "Cargo Bay";
Save(db, flak);
});
var before = File.ReadAllBytes(CatalogPath);
Assert.Throws<InvalidOperationException>(() => Program.GenericDesigns(apply: true, root: _root));
Assert.Equal(before, File.ReadAllBytes(CatalogPath));
}
[Fact]
public void A_brand_already_held_by_another_product_is_refused()
{
Edit(db =>
{
var product = ProductNamed(db, "Hammerfall");
product.Name = "Autocannon";
Save(db, product);
var other = ProductNamed(db, "Wren");
other.Name = "Hammerfall";
Save(db, other);
});
var before = File.ReadAllBytes(CatalogPath);
Assert.Throws<InvalidOperationException>(() => Program.GenericDesigns(apply: true, root: _root));
Assert.Equal(before, File.ReadAllBytes(CatalogPath));
}
[Fact]
public void A_row_whose_old_and_generic_names_are_both_absent_is_refused()
{
Edit(db =>
{
var gun = DesignNamed(db, "Light Machine Gun");
gun.Name = "Neither Name";
Save(db, gun);
});
var before = File.ReadAllBytes(CatalogPath);
Assert.Throws<InvalidOperationException>(() => Program.GenericDesigns(apply: true, root: _root));
Assert.Equal(before, File.ReadAllBytes(CatalogPath));
}
[Fact]
public void A_brand_row_whose_maker_sells_no_product_of_the_design_under_either_name_is_refused()
{
Edit(db =>
{
var product = ProductNamed(db, "Hammerfall");
product.Name = "Neither Name";
Save(db, product);
});
var before = File.ReadAllBytes(CatalogPath);
Assert.Throws<InvalidOperationException>(() => Program.GenericDesigns(apply: true, root: _root));
Assert.Equal(before, File.ReadAllBytes(CatalogPath));
}
[Fact]
public void A_shipped_authoring_note_is_stripped_from_a_product_the_tables_do_not_name()
{
Edit(db =>
{
var product = ProductNamed(db, "LockOn");
product.Description += Program.AuthoringNote;
Save(db, product);
});
Assert.Equal(0, Program.GenericDesigns(apply: true, root: _root));
Read(db =>
{
Assert.EndsWith("target.", ProductNamed(db, "LockOn").Description);
return 0;
});
}
}