using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; /// /// Drift guard for the WP5 CSV column dictionary (): every typed column of a /// model-backed driver map must name a real property on that driver's TagConfigModel]]>, /// and the registered typed-column set per driver must match the batch-plan column dictionary exactly. A /// model rename or a column-dictionary change breaks this test rather than silently corrupting import/export. /// [Trait("Category", "Unit")] public sealed class CsvColumnMapReflectionTests { [Fact] public void Every_model_backed_typed_column_maps_to_a_real_model_property() { foreach (var map in CsvColumnMap.All.Where(m => m.ModelType is not null)) { foreach (var col in map.Columns) { col.ModelProperty.ShouldNotBeNull( $"{map.DriverType} column '{col.Header}' is model-backed but declares no ModelProperty."); map.ModelType!.GetProperty(col.ModelProperty!) .ShouldNotBeNull( $"{map.DriverType} column '{col.Header}' maps to '{col.ModelProperty}', " + $"which is not a property on {map.ModelType!.Name}."); } } } [Fact] public void Model_less_maps_declare_no_model_property() { foreach (var map in CsvColumnMap.All.Where(m => m.ModelType is null)) { foreach (var col in map.Columns) { col.ModelProperty.ShouldBeNull( $"{map.DriverType} column '{col.Header}' is model-less but declares ModelProperty '{col.ModelProperty}'."); } } } [Theory] [InlineData(DriverTypeNames.Modbus, "Region", "Address", "ModbusDataType", "ByteOrder", "BitIndex", "StringLength", "Writable")] [InlineData(DriverTypeNames.S7, "Address", "S7DataType", "StringLength", "Writable")] [InlineData(DriverTypeNames.AbCip, "TagPath", "AbCipDataType", "Writable")] [InlineData(DriverTypeNames.AbLegacy, "Address", "AbLegacyDataType", "Writable")] [InlineData(DriverTypeNames.TwinCAT, "SymbolPath", "TwinCATDataType", "Writable")] [InlineData(DriverTypeNames.FOCAS, "Address", "FocasDataType")] [InlineData(DriverTypeNames.OpcUaClient, "NodeId")] [InlineData(DriverTypeNames.Galaxy, "AttributeRef")] [InlineData(CsvColumnMap.CalculationDriverType, "ScriptId", "ChangeTriggered", "TimerIntervalMs")] public void Registered_typed_columns_match_the_batch_plan_dictionary(string driverType, params string[] expected) { var map = CsvColumnMap.Resolve(driverType); map.ShouldNotBeNull($"No CSV map registered for driver '{driverType}'."); map!.Headers.ShouldBe(expected); } [Fact] public void HeadersFor_puts_typed_columns_between_common_leading_and_the_TagConfigJson_fallback() { var headers = CsvColumnMap.HeadersFor(DriverTypeNames.Modbus); // Leading common columns come first, in order. headers.Take(CsvColumnMap.CommonLeadingColumns.Count).ShouldBe(CsvColumnMap.CommonLeadingColumns); // The typed Modbus columns come next. headers.Skip(CsvColumnMap.CommonLeadingColumns.Count).Take(7) .ShouldBe(CsvColumnMap.Resolve(DriverTypeNames.Modbus)!.Headers); // TagConfigJson is always the trailing fallback. headers[^1].ShouldBe(CsvColumnMap.TagConfigJsonColumn); } [Fact] public void Unknown_driver_headers_are_the_common_set_only() { CsvColumnMap.Resolve("NoSuchDriver").ShouldBeNull(); CsvColumnMap.HeadersFor("NoSuchDriver").ShouldBe(CsvColumnMap.CommonColumns); } }