59ea02d971
Staged CSV tag import (upload -> parse -> review grid with per-row verdicts -> all-or-nothing commit) and export for the /raw tree Device/TagGroup. - CsvColumnMap: per-driver typed column <-> TagConfig JSON maps (model-backed for the 7 typed drivers, raw-key for Galaxy/Calculation) + the common column dictionary; typed columns merge OVER the TagConfigJson fallback (typed wins). - RawTagCsvMapper: pure parse (review rows + verdicts + typed/fallback provenance + intra-batch dup detection) and export (residual TagConfigJson) over the T0-2 CsvParser/CsvWriter. - RawTagCsvExportReader: DbContext-backed read helper (no IRawTreeService extension) enumerating a device's tags + reconstructing group paths. - RawCsvImportModal + RawCsvExportModal (data-URI download). - Reflection guard test (typed column -> real model property + batch-plan dictionary parity) + export->import round-trip property test. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
86 lines
3.8 KiB
C#
86 lines
3.8 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Drift guard for the WP5 CSV column dictionary (<see cref="CsvColumnMap"/>): every typed column of a
|
|
/// model-backed driver map must name a real property on that driver's <c><![CDATA[<Driver>TagConfigModel]]></c>,
|
|
/// 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.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
}
|