fix(templates): override rows on a derived template are not second definitions in the collision check (ScadaBridge#119)
CollisionDetector counted a derived template's own override row (same name as a parent member, IsInherited=false) as a second definition alongside the parent's row, so AddAttribute/AddAlarm/AddScript for a fresh name failed with a false "Naming collision" once any override existed. The inheritance walk now keeps a (type, name) shadow set: a member defined at a nearer level of the chain shadows the same member further up (effective member set, by name), including overrides at mid levels. Real collisions (e.g. against a composed module) still report. Regression tests in TemplateEngine.Tests (detector + service, attribute/alarm/ script, mid-level override, positive control); 6 fail without the fix. The #118 importer's additions-before-overrides ordering is now unnecessary but harmless and was left in place. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -307,6 +307,70 @@ public class TemplateServiceTests
|
||||
Assert.Equal(8, result.Value.TemplateId);
|
||||
}
|
||||
|
||||
private (Template parent, Template derived) SetupDerivedWithOverrides()
|
||||
{
|
||||
var parent = new Template("Base") { Id = 7 };
|
||||
parent.Attributes.Add(new TemplateAttribute("A") { Id = 70, TemplateId = 7, DataType = DataType.Float });
|
||||
parent.Alarms.Add(new TemplateAlarm("Al") { Id = 71, TemplateId = 7, TriggerType = AlarmTriggerType.ValueMatch });
|
||||
parent.Scripts.Add(new TemplateScript("Sc", "return 1;") { Id = 72, TemplateId = 7 });
|
||||
var derived = new Template("Slot") { Id = 8, ParentTemplateId = 7 };
|
||||
derived.Attributes.Add(new TemplateAttribute("A") { Id = 80, TemplateId = 8, DataType = DataType.Float, Value = "9" });
|
||||
derived.Alarms.Add(new TemplateAlarm("Al") { Id = 81, TemplateId = 8, TriggerType = AlarmTriggerType.ValueMatch });
|
||||
derived.Scripts.Add(new TemplateScript("Sc", "return 2;") { Id = 82, TemplateId = 8 });
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(8, It.IsAny<CancellationToken>())).ReturnsAsync(derived);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { parent, derived });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { parent, derived });
|
||||
return (parent, derived);
|
||||
}
|
||||
|
||||
/// <summary>ScadaBridge#119: a new attribute can be added once the derived template has an override.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddAttribute_ToDerivedTemplateWithOverrides_Succeeds()
|
||||
{
|
||||
SetupDerivedWithOverrides();
|
||||
var result = await _service.AddAttributeAsync(8, new TemplateAttribute("Fresh") { DataType = DataType.String, Value = "" }, "admin");
|
||||
Assert.True(result.IsSuccess, result.IsFailure ? result.Error : null);
|
||||
}
|
||||
|
||||
/// <summary>ScadaBridge#119: a new alarm can be added once the derived template has an override.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddAlarm_ToDerivedTemplateWithOverrides_Succeeds()
|
||||
{
|
||||
SetupDerivedWithOverrides();
|
||||
var alarm = new TemplateAlarm("Fresh") { PriorityLevel = 500, TriggerType = AlarmTriggerType.RangeViolation, TriggerConfiguration = """{"Max": 100}""" };
|
||||
var result = await _service.AddAlarmAsync(8, alarm, "admin");
|
||||
Assert.True(result.IsSuccess, result.IsFailure ? result.Error : null);
|
||||
}
|
||||
|
||||
/// <summary>ScadaBridge#119: a new script can be added once the derived template has an override.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddScript_ToDerivedTemplateWithOverrides_Succeeds()
|
||||
{
|
||||
SetupDerivedWithOverrides();
|
||||
var result = await _service.AddScriptAsync(8, new TemplateScript("Fresh", "return true;") { TriggerType = "Startup" }, "admin");
|
||||
Assert.True(result.IsSuccess, result.IsFailure ? result.Error : null);
|
||||
}
|
||||
|
||||
/// <summary>ScadaBridge#119 positive control: a genuine collision is still rejected on a template with overrides.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddAttribute_ToDerivedTemplateWithOverrides_RealCollisionStillRejected()
|
||||
{
|
||||
var (_, derived) = SetupDerivedWithOverrides();
|
||||
var module = new Template("Module") { Id = 3 };
|
||||
module.Attributes.Add(new TemplateAttribute("Shared") { Id = 30, TemplateId = 3, DataType = DataType.Float });
|
||||
derived.Compositions.Add(new TemplateComposition("dup") { Id = 1, TemplateId = 8, ComposedTemplateId = 3 });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { derived, module });
|
||||
var result = await _service.AddAttributeAsync(8, new TemplateAttribute("dup.Shared") { DataType = DataType.Float, Value = "" }, "admin");
|
||||
Assert.True(result.IsFailure);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add attribute, duplicate name, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user