f480a56737
Closes the operator parity gap the deferred-work register tracked as #12: native alarm source overrides could only be set one-at-a-time, while attribute overrides had a CSV bulk path. Adds an all-or-nothing CSV import for native sources. - Commons: extract the RFC-4180 line splitter into a shared CsvLineSplitter (refactor OverrideCsvParser onto it — no behavior change, pinned by its tests); new NativeAlarmSourceOverrideCsvParser (header SourceName,Connection, SourceReference,Filter; blank = inherited). - Commons: NativeAlarmSourceOverrideEntry + bulk SetInstanceNativeAlarmSource- OverridesCommand (auto-registered via the reflection command registry). - ManagementService: Deployer-gated handler — flattens once, validates every source resolves + is unlocked + no duplicates up front, then upserts the whole batch under a single SaveChanges (true all-or-nothing txn). Added to the frozen authorization matrix; dispatch-coverage guard passes. - CLI: `instance native-alarm-source import --instance-id --file` (parity with `instance import-overrides`) + README. - Tests: native parser (Commons), CLI parse/entry mapping, 3 bulk-handler tests (happy path single-commit, locked-source reject, unresolved-source reject). Also corrects a stale XML-doc line in ScriptRuntimeContext (WaitForAttribute quality-gated mode is shipped, not "planned") and updates the deferred-work register: marks #7/#13/#15/#16/#20 verified-resolved, #12 as CLI/API-shipped with only the Central UI upload affordance still pending. Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
149 lines
4.6 KiB
C#
149 lines
4.6 KiB
C#
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests;
|
|
|
|
public class NativeAlarmSourceOverrideCsvParserTests
|
|
{
|
|
[Fact]
|
|
public void Parse_SimpleFile_ReturnsRowsNoErrors()
|
|
{
|
|
const string csv =
|
|
"SourceName,Connection,SourceReference,Filter\n" +
|
|
"Pressure,ConnA,ns=2;s=Pump.Alarm,Active\n" +
|
|
"Module.Temp,ConnB,ns=2;s=Temp,\n";
|
|
|
|
var result = NativeAlarmSourceOverrideCsvParser.Parse(csv);
|
|
|
|
Assert.Empty(result.Errors);
|
|
Assert.Equal(2, result.Rows.Count);
|
|
|
|
Assert.Equal("Pressure", result.Rows[0].SourceName);
|
|
Assert.Equal("ConnA", result.Rows[0].Connection);
|
|
Assert.Equal("ns=2;s=Pump.Alarm", result.Rows[0].SourceReference);
|
|
Assert.Equal("Active", result.Rows[0].Filter);
|
|
Assert.Equal(2, result.Rows[0].LineNumber);
|
|
|
|
Assert.Equal("Module.Temp", result.Rows[1].SourceName);
|
|
Assert.Equal("ConnB", result.Rows[1].Connection);
|
|
Assert.Equal("ns=2;s=Temp", result.Rows[1].SourceReference);
|
|
Assert.Null(result.Rows[1].Filter);
|
|
Assert.Equal(3, result.Rows[1].LineNumber);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_BlankOverrideFields_BecomeNullInherited()
|
|
{
|
|
const string csv =
|
|
"SourceName,Connection,SourceReference,Filter\n" +
|
|
"Pressure,,,\n";
|
|
|
|
var result = NativeAlarmSourceOverrideCsvParser.Parse(csv);
|
|
|
|
Assert.Empty(result.Errors);
|
|
var row = Assert.Single(result.Rows);
|
|
Assert.Equal("Pressure", row.SourceName);
|
|
Assert.Null(row.Connection);
|
|
Assert.Null(row.SourceReference);
|
|
Assert.Null(row.Filter);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_HeaderCaseInsensitive_Accepted()
|
|
{
|
|
const string csv =
|
|
"sourcename,CONNECTION,SourceReference,filter\n" +
|
|
"Pressure,ConnA,ref,\n";
|
|
|
|
var result = NativeAlarmSourceOverrideCsvParser.Parse(csv);
|
|
|
|
Assert.Empty(result.Errors);
|
|
Assert.Single(result.Rows);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_QuotedFieldWithComma_PreservesEmbeddedComma()
|
|
{
|
|
const string csv =
|
|
"SourceName,Connection,SourceReference,Filter\n" +
|
|
"Pressure,ConnA,\"ns=2;s=a,b,c\",Active\n";
|
|
|
|
var result = NativeAlarmSourceOverrideCsvParser.Parse(csv);
|
|
|
|
Assert.Empty(result.Errors);
|
|
var row = Assert.Single(result.Rows);
|
|
Assert.Equal("ns=2;s=a,b,c", row.SourceReference);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_MissingHeader_ReturnsHeaderError()
|
|
{
|
|
const string csv = "Pressure,ConnA,ref,Active\n";
|
|
|
|
var result = NativeAlarmSourceOverrideCsvParser.Parse(csv);
|
|
|
|
Assert.Empty(result.Rows);
|
|
var error = Assert.Single(result.Errors);
|
|
Assert.Contains("SourceName,Connection,SourceReference,Filter", error);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_WrongColumnCount_ReportsPerLineErrorAndSkipsRow()
|
|
{
|
|
const string csv =
|
|
"SourceName,Connection,SourceReference,Filter\n" +
|
|
"Pressure,ConnA\n" +
|
|
"Temp,ConnB,ref,Active\n";
|
|
|
|
var result = NativeAlarmSourceOverrideCsvParser.Parse(csv);
|
|
|
|
var error = Assert.Single(result.Errors);
|
|
Assert.Contains("Line 2", error);
|
|
Assert.Contains("expected 4 columns", error);
|
|
// Valid rows still flow through.
|
|
var row = Assert.Single(result.Rows);
|
|
Assert.Equal("Temp", row.SourceName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_BlankSourceName_ReportsError()
|
|
{
|
|
const string csv =
|
|
"SourceName,Connection,SourceReference,Filter\n" +
|
|
",ConnA,ref,Active\n";
|
|
|
|
var result = NativeAlarmSourceOverrideCsvParser.Parse(csv);
|
|
|
|
Assert.Empty(result.Rows);
|
|
var error = Assert.Single(result.Errors);
|
|
Assert.Contains("SourceName must not be blank", error);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_UnterminatedQuote_ReportsError()
|
|
{
|
|
const string csv =
|
|
"SourceName,Connection,SourceReference,Filter\n" +
|
|
"Pressure,ConnA,\"ns=2;s=open,Active\n";
|
|
|
|
var result = NativeAlarmSourceOverrideCsvParser.Parse(csv);
|
|
|
|
Assert.Empty(result.Rows);
|
|
Assert.Contains(result.Errors, e => e.Contains("Unterminated quoted field"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_BlankLinesSkippedWithoutError()
|
|
{
|
|
const string csv =
|
|
"SourceName,Connection,SourceReference,Filter\n" +
|
|
"\n" +
|
|
"Pressure,ConnA,ref,Active\n" +
|
|
" \n";
|
|
|
|
var result = NativeAlarmSourceOverrideCsvParser.Parse(csv);
|
|
|
|
Assert.Empty(result.Errors);
|
|
Assert.Single(result.Rows);
|
|
}
|
|
}
|