fix(build): run the persistence generator against the current data model
The PreBuild targets ran the generator with "--no-build", so it used whatever assemblies happened to sit in its output folder. When that copy of the data model was older than a newly added type, the generator regenerated the checked-in *.Generated.cs files WITHOUT that type and overwrote them in the source tree. Nothing failed at build time: the C# compile stayed green and docker builds (-p:ci=true) skip the generator entirely, so they compiled whatever was in the tree. The damage only surfaced at runtime, when EF validated the model and found the inherited GameConfiguration.CastleSiegeConfiguration navigation pointing at a keyless type - the server died on startup with "The entity type 'CastleSiegeConfiguration' requires a primary key to be defined". Dropping the switch makes the generator build first, so its output always matches the data model. The regenerated files here are that missing output: the Castle Siege mappings, and the packet tests for packets whose XML was already committed. TypedContextModelTests builds the typed context the startup reads its plugin configurations through - the first one to touch the model - so this class of breakage fails in seconds without a database. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -484,6 +484,24 @@ public partial class GameConfiguration : MUnique.OpenMU.DataModel.Configuration.
|
||||
set => base.DuelConfiguration = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw object of <see cref="CastleSiegeConfiguration" />.
|
||||
/// </summary>
|
||||
[System.Text.Json.Serialization.JsonPropertyName("castleSiegeConfiguration")]
|
||||
public CastleSiegeConfiguration RawCastleSiegeConfiguration
|
||||
{
|
||||
get => base.CastleSiegeConfiguration as CastleSiegeConfiguration;
|
||||
set => base.CastleSiegeConfiguration = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[System.Text.Json.Serialization.JsonIgnore]
|
||||
public override MUnique.OpenMU.DataModel.Configuration.CastleSiegeConfiguration CastleSiegeConfiguration
|
||||
{
|
||||
get => base.CastleSiegeConfiguration;
|
||||
set => base.CastleSiegeConfiguration = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override MUnique.OpenMU.DataModel.Configuration.GameConfiguration Clone(MUnique.OpenMU.DataModel.Configuration.GameConfiguration gameConfiguration)
|
||||
{
|
||||
|
||||
@@ -48,8 +48,15 @@
|
||||
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
The generator overwrites the checked-in *.Generated.cs files, so it must run against the current data
|
||||
model. Do NOT add the "no build" switch here: it would reuse whatever assemblies happen to lie in the
|
||||
generator's output folder, and a stale copy of the data model silently regenerates the model files
|
||||
without the types added since. The build still succeeds and only fails at runtime, when EF validates
|
||||
the model.
|
||||
-->
|
||||
<Target Name="PreBuild" BeforeTargets="PreBuildEvent" Condition="'$(ci)'!='true'">
|
||||
<Exec Command="dotnet run --project ../SourceGenerator/MUnique.OpenMU.Persistence.SourceGenerator.csproj -c:$(ConfigurationName) MUnique.OpenMU.Persistence.EntityFramework "$(ProjectDir)Model" --no-build" />
|
||||
<Exec Command="dotnet run --project ../SourceGenerator/MUnique.OpenMU.Persistence.SourceGenerator.csproj -c:$(ConfigurationName) MUnique.OpenMU.Persistence.EntityFramework "$(ProjectDir)Model"" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -27,6 +27,9 @@ public class ExtendedTypeContext : Microsoft.EntityFrameworkCore.DbContext
|
||||
modelBuilder.Ignore<MUnique.OpenMU.DataModel.Statistics.MiniGameRankingEntry>();
|
||||
modelBuilder.Ignore<MUnique.OpenMU.DataModel.Entities.Account>();
|
||||
modelBuilder.Ignore<MUnique.OpenMU.DataModel.Entities.AppearanceData>();
|
||||
modelBuilder.Ignore<MUnique.OpenMU.DataModel.Entities.CastleSiegeData>();
|
||||
modelBuilder.Ignore<MUnique.OpenMU.DataModel.Entities.CastleSiegeGuildRegistration>();
|
||||
modelBuilder.Ignore<MUnique.OpenMU.DataModel.Entities.CastleSiegeNpcState>();
|
||||
modelBuilder.Ignore<MUnique.OpenMU.DataModel.Entities.Character>();
|
||||
modelBuilder.Ignore<MUnique.OpenMU.DataModel.Entities.CharacterQuestState>();
|
||||
modelBuilder.Ignore<MUnique.OpenMU.DataModel.Entities.Guild>();
|
||||
@@ -41,6 +44,11 @@ public class ExtendedTypeContext : Microsoft.EntityFrameworkCore.DbContext
|
||||
modelBuilder.Ignore<MUnique.OpenMU.DataModel.Configuration.AreaSkillSettings>();
|
||||
modelBuilder.Ignore<MUnique.OpenMU.DataModel.Configuration.BattleZoneDefinition>();
|
||||
modelBuilder.Ignore<MUnique.OpenMU.DataModel.Configuration.Buff>();
|
||||
modelBuilder.Ignore<MUnique.OpenMU.DataModel.Configuration.CastleSiegeConfiguration>();
|
||||
modelBuilder.Ignore<MUnique.OpenMU.DataModel.Configuration.CastleSiegeNpcDefinition>();
|
||||
modelBuilder.Ignore<MUnique.OpenMU.DataModel.Configuration.CastleSiegeStateScheduleEntry>();
|
||||
modelBuilder.Ignore<MUnique.OpenMU.DataModel.Configuration.CastleSiegeUpgradeDefinition>();
|
||||
modelBuilder.Ignore<MUnique.OpenMU.DataModel.Configuration.CastleSiegeZoneDefinition>();
|
||||
modelBuilder.Ignore<MUnique.OpenMU.DataModel.Configuration.CharacterClass>();
|
||||
modelBuilder.Ignore<MUnique.OpenMU.DataModel.Configuration.ChatServerDefinition>();
|
||||
modelBuilder.Ignore<MUnique.OpenMU.DataModel.Configuration.ChatServerEndpoint>();
|
||||
@@ -118,6 +126,7 @@ public class ExtendedTypeContext : Microsoft.EntityFrameworkCore.DbContext
|
||||
modelBuilder.Entity<Account>().HasMany(entity => entity.RawCharacters).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<Account>().HasMany(entity => entity.RawAttributes).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<AppearanceData>().HasMany(entity => entity.RawEquippedItems).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<CastleSiegeData>().HasMany(entity => entity.RawNpcStates).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<Character>().HasMany(entity => entity.RawAttributes).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<Character>().HasMany(entity => entity.RawLetters).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<Character>().HasMany(entity => entity.RawLearnedSkills).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
@@ -131,6 +140,17 @@ public class ExtendedTypeContext : Microsoft.EntityFrameworkCore.DbContext
|
||||
modelBuilder.Entity<BattleZoneDefinition>().HasOne(entity => entity.RawLeftGoal).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<BattleZoneDefinition>().HasOne(entity => entity.RawRightGoal).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<Buff>().HasOne(entity => entity.RawMagicEffectDefinition).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<CastleSiegeConfiguration>().HasMany(entity => entity.RawStateSchedule).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<CastleSiegeConfiguration>().HasMany(entity => entity.RawNpcDefinitions).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<CastleSiegeConfiguration>().HasMany(entity => entity.RawGateDefenseUpgrades).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<CastleSiegeConfiguration>().HasMany(entity => entity.RawGateLifeUpgrades).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<CastleSiegeConfiguration>().HasMany(entity => entity.RawStatueDefenseUpgrades).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<CastleSiegeConfiguration>().HasMany(entity => entity.RawStatueLifeUpgrades).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<CastleSiegeConfiguration>().HasMany(entity => entity.RawStatueRegenUpgrades).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<CastleSiegeConfiguration>().HasMany(entity => entity.RawAttackMachineZones).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<CastleSiegeConfiguration>().HasMany(entity => entity.RawDefenseMachineZones).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<CastleSiegeConfiguration>().HasOne(entity => entity.RawDefenseRespawnArea).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<CastleSiegeConfiguration>().HasOne(entity => entity.RawAttackRespawnArea).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<CharacterClass>().HasMany(entity => entity.RawStatAttributes).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<CharacterClass>().HasMany(entity => entity.RawAttributeCombinations).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<CharacterClass>().HasMany(entity => entity.RawBaseAttributeValues).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
@@ -159,6 +179,7 @@ public class ExtendedTypeContext : Microsoft.EntityFrameworkCore.DbContext
|
||||
modelBuilder.Entity<GameConfiguration>().HasMany(entity => entity.RawGlobalBaseAttributeValues).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<GameConfiguration>().HasMany(entity => entity.RawPlugInConfigurations).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<GameConfiguration>().HasMany(entity => entity.RawMiniGameDefinitions).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<GameConfiguration>().HasOne(entity => entity.RawCastleSiegeConfiguration).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<GameMapDefinition>().HasMany(entity => entity.RawMonsterSpawns).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<GameMapDefinition>().HasMany(entity => entity.RawEnterGates).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<GameMapDefinition>().HasOne(entity => entity.RawBattleZone).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
@@ -243,6 +243,32 @@ internal partial class GameConfiguration : MUnique.OpenMU.DataModel.Configuratio
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the identifier of <see cref="CastleSiegeConfiguration"/>.
|
||||
/// </summary>
|
||||
public Guid? CastleSiegeConfigurationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw object of <see cref="CastleSiegeConfiguration" />.
|
||||
/// </summary>
|
||||
[ForeignKey(nameof(CastleSiegeConfigurationId))]
|
||||
public CastleSiegeConfiguration RawCastleSiegeConfiguration
|
||||
{
|
||||
get => base.CastleSiegeConfiguration as CastleSiegeConfiguration;
|
||||
set => base.CastleSiegeConfiguration = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[NotMapped]
|
||||
public override MUnique.OpenMU.DataModel.Configuration.CastleSiegeConfiguration CastleSiegeConfiguration
|
||||
{
|
||||
get => base.CastleSiegeConfiguration;set
|
||||
{
|
||||
base.CastleSiegeConfiguration = value;
|
||||
this.CastleSiegeConfigurationId = this.RawCastleSiegeConfiguration?.Id;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override MUnique.OpenMU.DataModel.Configuration.GameConfiguration Clone(MUnique.OpenMU.DataModel.Configuration.GameConfiguration gameConfiguration)
|
||||
{
|
||||
|
||||
@@ -44,6 +44,15 @@ public static class MapsterConfigurator
|
||||
Mapster.TypeAdapterConfig.GlobalSettings.NewConfig<MUnique.OpenMU.DataModel.Entities.AppearanceData, MUnique.OpenMU.DataModel.Entities.AppearanceData>()
|
||||
.Include<AppearanceData, BasicModel.AppearanceData>();
|
||||
|
||||
Mapster.TypeAdapterConfig.GlobalSettings.NewConfig<MUnique.OpenMU.DataModel.Entities.CastleSiegeData, MUnique.OpenMU.DataModel.Entities.CastleSiegeData>()
|
||||
.Include<CastleSiegeData, BasicModel.CastleSiegeData>();
|
||||
|
||||
Mapster.TypeAdapterConfig.GlobalSettings.NewConfig<MUnique.OpenMU.DataModel.Entities.CastleSiegeGuildRegistration, MUnique.OpenMU.DataModel.Entities.CastleSiegeGuildRegistration>()
|
||||
.Include<CastleSiegeGuildRegistration, BasicModel.CastleSiegeGuildRegistration>();
|
||||
|
||||
Mapster.TypeAdapterConfig.GlobalSettings.NewConfig<MUnique.OpenMU.DataModel.Entities.CastleSiegeNpcState, MUnique.OpenMU.DataModel.Entities.CastleSiegeNpcState>()
|
||||
.Include<CastleSiegeNpcState, BasicModel.CastleSiegeNpcState>();
|
||||
|
||||
Mapster.TypeAdapterConfig.GlobalSettings.NewConfig<MUnique.OpenMU.DataModel.Entities.Character, MUnique.OpenMU.DataModel.Entities.Character>()
|
||||
.Include<Character, BasicModel.Character>();
|
||||
|
||||
@@ -86,6 +95,21 @@ public static class MapsterConfigurator
|
||||
Mapster.TypeAdapterConfig.GlobalSettings.NewConfig<MUnique.OpenMU.DataModel.Configuration.Buff, MUnique.OpenMU.DataModel.Configuration.Buff>()
|
||||
.Include<Buff, BasicModel.Buff>();
|
||||
|
||||
Mapster.TypeAdapterConfig.GlobalSettings.NewConfig<MUnique.OpenMU.DataModel.Configuration.CastleSiegeConfiguration, MUnique.OpenMU.DataModel.Configuration.CastleSiegeConfiguration>()
|
||||
.Include<CastleSiegeConfiguration, BasicModel.CastleSiegeConfiguration>();
|
||||
|
||||
Mapster.TypeAdapterConfig.GlobalSettings.NewConfig<MUnique.OpenMU.DataModel.Configuration.CastleSiegeNpcDefinition, MUnique.OpenMU.DataModel.Configuration.CastleSiegeNpcDefinition>()
|
||||
.Include<CastleSiegeNpcDefinition, BasicModel.CastleSiegeNpcDefinition>();
|
||||
|
||||
Mapster.TypeAdapterConfig.GlobalSettings.NewConfig<MUnique.OpenMU.DataModel.Configuration.CastleSiegeStateScheduleEntry, MUnique.OpenMU.DataModel.Configuration.CastleSiegeStateScheduleEntry>()
|
||||
.Include<CastleSiegeStateScheduleEntry, BasicModel.CastleSiegeStateScheduleEntry>();
|
||||
|
||||
Mapster.TypeAdapterConfig.GlobalSettings.NewConfig<MUnique.OpenMU.DataModel.Configuration.CastleSiegeUpgradeDefinition, MUnique.OpenMU.DataModel.Configuration.CastleSiegeUpgradeDefinition>()
|
||||
.Include<CastleSiegeUpgradeDefinition, BasicModel.CastleSiegeUpgradeDefinition>();
|
||||
|
||||
Mapster.TypeAdapterConfig.GlobalSettings.NewConfig<MUnique.OpenMU.DataModel.Configuration.CastleSiegeZoneDefinition, MUnique.OpenMU.DataModel.Configuration.CastleSiegeZoneDefinition>()
|
||||
.Include<CastleSiegeZoneDefinition, BasicModel.CastleSiegeZoneDefinition>();
|
||||
|
||||
Mapster.TypeAdapterConfig.GlobalSettings.NewConfig<MUnique.OpenMU.DataModel.Configuration.CharacterClass, MUnique.OpenMU.DataModel.Configuration.CharacterClass>()
|
||||
.Include<CharacterClass, BasicModel.CharacterClass>();
|
||||
|
||||
|
||||
@@ -41,8 +41,15 @@
|
||||
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
The generator overwrites the checked-in *.Generated.cs files, so it must run against the current data
|
||||
model. Do NOT add the "no build" switch here: it would reuse whatever assemblies happen to lie in the
|
||||
generator's output folder, and a stale copy of the data model silently regenerates the model files
|
||||
without the types added since. The build still succeeds and only fails at runtime, when EF validates
|
||||
the model.
|
||||
-->
|
||||
<Target Name="PreBuild" BeforeTargets="PreBuildEvent" Condition="'$(ci)'!='true'">
|
||||
<Exec Command="dotnet run --project SourceGenerator/MUnique.OpenMU.Persistence.SourceGenerator.csproj -c:$(ConfigurationName) MUnique.OpenMU.Persistence "$(ProjectDir)BasicModel" --no-build" />
|
||||
<Exec Command="dotnet run --project SourceGenerator/MUnique.OpenMU.Persistence.SourceGenerator.csproj -c:$(ConfigurationName) MUnique.OpenMU.Persistence "$(ProjectDir)BasicModel"" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -3904,6 +3904,24 @@ public class PacketStructureTests
|
||||
"Packet length mismatch: declared length does not match calculated size");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for HeykelSavasiTeamSelect.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void HeykelSavasiTeamSelect_PacketSizeValidation()
|
||||
{
|
||||
// Fixed-length packet validation
|
||||
const int expectedLength = 4;
|
||||
var actualLength = HeykelSavasiTeamSelectRef.Length;
|
||||
|
||||
Assert.That(actualLength, Is.EqualTo(expectedLength),
|
||||
"Packet length mismatch: declared length does not match calculated size");
|
||||
|
||||
// Validate field 'Team' boundary
|
||||
Assert.That(3 + 1, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'Team' exceeds packet boundary");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for ChatCommandListRequest.
|
||||
/// </summary>
|
||||
|
||||
@@ -6415,10 +6415,102 @@ public class PacketStructureTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for ChatCommandInfo.
|
||||
/// Tests the packet size calculation for HeykelSavasiOpenTeamPanel.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ChatCommandInfo_PacketSizeValidation()
|
||||
public void HeykelSavasiOpenTeamPanel_PacketSizeValidation()
|
||||
{
|
||||
// Fixed-length packet validation
|
||||
const int expectedLength = 5;
|
||||
var actualLength = HeykelSavasiOpenTeamPanelRef.Length;
|
||||
|
||||
Assert.That(actualLength, Is.EqualTo(expectedLength),
|
||||
"Packet length mismatch: declared length does not match calculated size");
|
||||
|
||||
// Validate field 'RedCount' boundary
|
||||
Assert.That(3 + 1, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'RedCount' exceeds packet boundary");
|
||||
|
||||
// Validate field 'BlueCount' boundary
|
||||
Assert.That(4 + 1, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'BlueCount' exceeds packet boundary");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for HeykelSavasiHudState.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void HeykelSavasiHudState_PacketSizeValidation()
|
||||
{
|
||||
// Fixed-length packet validation
|
||||
const int expectedLength = 11;
|
||||
var actualLength = HeykelSavasiHudStateRef.Length;
|
||||
|
||||
Assert.That(actualLength, Is.EqualTo(expectedLength),
|
||||
"Packet length mismatch: declared length does not match calculated size");
|
||||
|
||||
// Validate field 'Phase' boundary
|
||||
Assert.That(3 + 1, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'Phase' exceeds packet boundary");
|
||||
|
||||
// Validate field 'MyTeam' boundary
|
||||
Assert.That(4 + 1, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'MyTeam' exceeds packet boundary");
|
||||
|
||||
// Validate field 'RedCount' boundary
|
||||
Assert.That(5 + 1, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'RedCount' exceeds packet boundary");
|
||||
|
||||
// Validate field 'BlueCount' boundary
|
||||
Assert.That(6 + 1, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'BlueCount' exceeds packet boundary");
|
||||
|
||||
// Validate field 'RedProgress' boundary
|
||||
Assert.That(7 + 1, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'RedProgress' exceeds packet boundary");
|
||||
|
||||
// Validate field 'BlueProgress' boundary
|
||||
Assert.That(8 + 1, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'BlueProgress' exceeds packet boundary");
|
||||
|
||||
// Validate field 'RemainingSeconds' boundary
|
||||
Assert.That(9 + 2, Is.LessThanOrEqualTo(expectedLength),
|
||||
"Field 'RemainingSeconds' exceeds packet boundary");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for HeykelSavasiTeamRoster.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void HeykelSavasiTeamRoster_PacketSizeValidation()
|
||||
{
|
||||
// Basic packet validation
|
||||
// Validate header type and field boundaries
|
||||
|
||||
// Field 'Count' starts at index 3 with size 1
|
||||
Assert.That(3, Is.GreaterThanOrEqualTo(0),
|
||||
"Field 'Count' has invalid negative index");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for HeykelSavasiScoreboard.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void HeykelSavasiScoreboard_PacketSizeValidation()
|
||||
{
|
||||
// Basic packet validation
|
||||
// Validate header type and field boundaries
|
||||
|
||||
// Field 'Count' starts at index 3 with size 1
|
||||
Assert.That(3, Is.GreaterThanOrEqualTo(0),
|
||||
"Field 'Count' has invalid negative index");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the packet size calculation for AvailableChatCommand.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void AvailableChatCommand_PacketSizeValidation()
|
||||
{
|
||||
// Basic packet validation
|
||||
// Validate header type and field boundaries
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// <copyright file="TypedContextModelTests.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Persistence.Initialization.Tests;
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// Tests that the typed contexts can build their entity model. A typed context keeps only the edited type
|
||||
/// (plus its aggregate) and ignores every other type, so a type which is only mapped in the full context
|
||||
/// slips through the build and blows up at runtime instead. The startup reads the plugin configurations
|
||||
/// through such a context before anything else, so a broken model there means the server doesn't start.
|
||||
/// No database is needed: building the model already runs the EF model validation.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
internal class TypedContextModelTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds the model of the typed context which the startup uses to read the plugin configurations.
|
||||
/// A failing connection is fine here (there may be no database); a failing model is not.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void PlugInConfigurationContextBuildsModel()
|
||||
{
|
||||
var provider = new PersistenceContextProvider(new NullLoggerFactory(), null);
|
||||
using var context = provider.CreateNewTypedContext(typeof(PlugInConfiguration), false);
|
||||
|
||||
try
|
||||
{
|
||||
_ = context.GetAsync<PlugInConfiguration>().AsTask().GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AssertNoModelError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertNoModelError(Exception exception)
|
||||
{
|
||||
for (var ex = exception; ex is not null; ex = ex.InnerException!)
|
||||
{
|
||||
if (ex is InvalidOperationException && ex.Message.Contains("requires a primary key"))
|
||||
{
|
||||
Assert.Fail($"The entity model of the typed context is broken: {ex.Message}");
|
||||
}
|
||||
|
||||
if (ex.InnerException is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user