Skip to content

Commit 74c3cb0

Browse files
Copilotromanettmarcschier
authored
Fix: State helper methods now update timestamps and clear change masks (#3371)
* Initial plan * Fix: Add timestamp updates and ClearChangeMasks calls to State helper methods Co-authored-by: romanett <[email protected]> * Add tests for State helper method timestamp and change mask updates Co-authored-by: romanett <[email protected]> * Improve test reliability by removing Thread.Sleep Co-authored-by: romanett <[email protected]> * Resolve merge conflict with master by incorporating EvaluateRetainStateOnEnable method Co-authored-by: marcschier <[email protected]> * rename for merge * merge update --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: romanett <[email protected]> Co-authored-by: marcschier <[email protected]> Co-authored-by: Roman Ettlinger <[email protected]>
1 parent 3f660af commit 74c3cb0

File tree

4 files changed

+218
-0
lines changed

4 files changed

+218
-0
lines changed

Stack/Opc.Ua.Core/Stack/State/AcknowledgeableConditionState.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ public virtual void SetAcknowledgedState(ISystemContext context, bool acknowledg
6666
{
6767
UpdateStateAfterUnacknowledge(context);
6868
}
69+
70+
AckedState.Timestamp = DateTime.UtcNow;
71+
ClearChangeMasks(context, includeChildren: true);
6972
}
7073

7174
/// <summary>
@@ -83,6 +86,13 @@ public virtual void SetConfirmedState(ISystemContext context, bool confirmed)
8386
{
8487
UpdateStateAfterUnconfirm(context);
8588
}
89+
90+
if (ConfirmedState != null)
91+
{
92+
ConfirmedState.Timestamp = DateTime.UtcNow;
93+
}
94+
95+
ClearChangeMasks(context, includeChildren: true);
8696
}
8797

8898
/// <summary>

Stack/Opc.Ua.Core/Stack/State/AlarmConditionState.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,9 @@ public virtual void SetActiveState(ISystemContext context, bool active)
177177
ActiveState.TransitionTime.Value = DateTime.UtcNow;
178178
}
179179

180+
ActiveState.Timestamp = DateTime.UtcNow;
180181
UpdateEffectiveState(context);
182+
ClearChangeMasks(context, includeChildren: true);
181183
}
182184

183185
/// <summary>
@@ -225,7 +227,9 @@ public virtual void SetSuppressedState(ISystemContext context, bool suppressed)
225227
SuppressedState.TransitionTime.Value = DateTime.UtcNow;
226228
}
227229

230+
SuppressedState.Timestamp = DateTime.UtcNow;
228231
UpdateEffectiveState(context);
232+
ClearChangeMasks(context, includeChildren: true);
229233
}
230234

231235
/// <summary>
@@ -315,7 +319,13 @@ public virtual void SetShelvingState(
315319
ShelvingState.CauseProcessingCompleted(context, state);
316320
}
317321

322+
if (ShelvingState.UnshelveTime != null)
323+
{
324+
ShelvingState.UnshelveTime.Timestamp = DateTime.UtcNow;
325+
}
326+
318327
UpdateEffectiveState(context);
328+
ClearChangeMasks(context, includeChildren: true);
319329
}
320330

321331
/// <summary>

Stack/Opc.Ua.Core/Stack/State/ConditionState.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ public virtual void SetEnableState(ISystemContext context, bool enabled)
126126
{
127127
UpdateStateAfterDisable(context);
128128
}
129+
130+
EnabledState.Timestamp = DateTime.UtcNow;
131+
ClearChangeMasks(context, includeChildren: true);
129132
}
130133

131134
/// <summary>
@@ -143,6 +146,10 @@ public virtual void SetSeverity(ISystemContext context, EventSeverity severity)
143146
{
144147
LastSeverity.SourceTimestamp.Value = DateTime.UtcNow;
145148
}
149+
150+
LastSeverity.Timestamp = DateTime.UtcNow;
151+
Severity.Timestamp = DateTime.UtcNow;
152+
ClearChangeMasks(context, includeChildren: true);
146153
}
147154

148155
/// <summary>

Tests/Opc.Ua.Core.Tests/Stack/State/ConditionStateTests.cs

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
using NUnit.Framework;
3131
using Opc.Ua.Tests;
32+
using System;
3233

3334
namespace Opc.Ua.Core.Tests.Stack.State
3435
{
@@ -64,6 +65,196 @@ protected void OneTimeTearDown()
6465
Utils.SilentDispose(m_context);
6566
}
6667

68+
/// <summary>
69+
/// Test that SetEnableState updates the timestamp and clears change masks.
70+
/// </summary>
71+
[Test]
72+
public void SetEnableStateUpdatesTimestampAndClearsChangeMasks()
73+
{
74+
var condition = new ConditionState(null);
75+
condition.Create(m_context, new NodeId(1), "Condition", null, true);
76+
77+
// Set initial state
78+
var beforeTime = DateTime.UtcNow;
79+
condition.SetEnableState(m_context, true);
80+
var afterTime = DateTime.UtcNow;
81+
82+
// Verify timestamp is updated
83+
Assert.That(condition.EnabledState.Timestamp, Is.GreaterThanOrEqualTo(beforeTime));
84+
Assert.That(condition.EnabledState.Timestamp, Is.LessThanOrEqualTo(afterTime));
85+
86+
// Verify change masks are cleared (all should be None)
87+
Assert.That(condition.ChangeMasks, Is.EqualTo(NodeStateChangeMasks.None));
88+
Assert.That(condition.EnabledState.ChangeMasks, Is.EqualTo(NodeStateChangeMasks.None));
89+
}
90+
91+
/// <summary>
92+
/// Test that SetSeverity updates the timestamp and clears change masks.
93+
/// </summary>
94+
[Test]
95+
public void SetSeverityUpdatesTimestampAndClearsChangeMasks()
96+
{
97+
var condition = new ConditionState(null);
98+
condition.Create(m_context, new NodeId(1), "Condition", null, true);
99+
100+
var beforeTime = DateTime.UtcNow;
101+
condition.SetSeverity(m_context, EventSeverity.High);
102+
var afterTime = DateTime.UtcNow;
103+
104+
// Verify timestamps are updated
105+
Assert.That(condition.Severity.Timestamp, Is.GreaterThanOrEqualTo(beforeTime));
106+
Assert.That(condition.Severity.Timestamp, Is.LessThanOrEqualTo(afterTime));
107+
Assert.That(condition.LastSeverity.Timestamp, Is.GreaterThanOrEqualTo(beforeTime));
108+
Assert.That(condition.LastSeverity.Timestamp, Is.LessThanOrEqualTo(afterTime));
109+
110+
// Verify change masks are cleared
111+
Assert.That(condition.ChangeMasks, Is.EqualTo(NodeStateChangeMasks.None));
112+
}
113+
114+
/// <summary>
115+
/// Test that SetActiveState updates the timestamp and clears change masks.
116+
/// </summary>
117+
[Test]
118+
public void SetActiveStateUpdatesTimestampAndClearsChangeMasks()
119+
{
120+
var alarm = new AlarmConditionState(m_telemetry, null);
121+
alarm.Create(m_context, new NodeId(1), "Alarm", null, true);
122+
123+
var beforeTime = DateTime.UtcNow;
124+
alarm.SetActiveState(m_context, true);
125+
var afterTime = DateTime.UtcNow;
126+
127+
// Verify timestamp is updated
128+
Assert.That(alarm.ActiveState.Timestamp, Is.GreaterThanOrEqualTo(beforeTime));
129+
Assert.That(alarm.ActiveState.Timestamp, Is.LessThanOrEqualTo(afterTime));
130+
131+
// Verify change masks are cleared
132+
Assert.That(alarm.ChangeMasks, Is.EqualTo(NodeStateChangeMasks.None));
133+
Assert.That(alarm.ActiveState.ChangeMasks, Is.EqualTo(NodeStateChangeMasks.None));
134+
}
135+
136+
/// <summary>
137+
/// Test that SetSuppressedState updates the timestamp and clears change masks.
138+
/// </summary>
139+
[Test]
140+
public void SetSuppressedStateUpdatesTimestampAndClearsChangeMasks()
141+
{
142+
var alarm = new AlarmConditionState(m_telemetry, null);
143+
alarm.Create(m_context, new NodeId(1), "Alarm", null, true);
144+
alarm.SuppressedState = new TwoStateVariableState(alarm);
145+
alarm.SuppressedState.Create(m_context, null, BrowseNames.SuppressedState, null, false);
146+
147+
var beforeTime = DateTime.UtcNow;
148+
alarm.SetSuppressedState(m_context, true);
149+
var afterTime = DateTime.UtcNow;
150+
151+
// Verify timestamp is updated
152+
Assert.That(alarm.SuppressedState.Timestamp, Is.GreaterThanOrEqualTo(beforeTime));
153+
Assert.That(alarm.SuppressedState.Timestamp, Is.LessThanOrEqualTo(afterTime));
154+
155+
// Verify change masks are cleared
156+
Assert.That(alarm.ChangeMasks, Is.EqualTo(NodeStateChangeMasks.None));
157+
Assert.That(alarm.SuppressedState.ChangeMasks, Is.EqualTo(NodeStateChangeMasks.None));
158+
}
159+
160+
/// <summary>
161+
/// Test that SetAcknowledgedState updates the timestamp and clears change masks.
162+
/// </summary>
163+
[Test]
164+
public void SetAcknowledgedStateUpdatesTimestampAndClearsChangeMasks()
165+
{
166+
var condition = new AcknowledgeableConditionState(null);
167+
condition.Create(m_context, new NodeId(1), "AckCondition", null, true);
168+
169+
var beforeTime = DateTime.UtcNow;
170+
condition.SetAcknowledgedState(m_context, true);
171+
var afterTime = DateTime.UtcNow;
172+
173+
// Verify timestamp is updated
174+
Assert.That(condition.AckedState.Timestamp, Is.GreaterThanOrEqualTo(beforeTime));
175+
Assert.That(condition.AckedState.Timestamp, Is.LessThanOrEqualTo(afterTime));
176+
177+
// Verify change masks are cleared
178+
Assert.That(condition.ChangeMasks, Is.EqualTo(NodeStateChangeMasks.None));
179+
Assert.That(condition.AckedState.ChangeMasks, Is.EqualTo(NodeStateChangeMasks.None));
180+
}
181+
182+
/// <summary>
183+
/// Test that SetConfirmedState updates the timestamp and clears change masks.
184+
/// </summary>
185+
[Test]
186+
public void SetConfirmedStateUpdatesTimestampAndClearsChangeMasks()
187+
{
188+
var condition = new AcknowledgeableConditionState(null);
189+
condition.Create(m_context, new NodeId(1), "AckCondition", null, true);
190+
condition.ConfirmedState = new TwoStateVariableState(condition);
191+
condition.ConfirmedState.Create(m_context, null, BrowseNames.ConfirmedState, null, false);
192+
193+
var beforeTime = DateTime.UtcNow;
194+
condition.SetConfirmedState(m_context, true);
195+
var afterTime = DateTime.UtcNow;
196+
197+
// Verify timestamp is updated
198+
Assert.That(condition.ConfirmedState.Timestamp, Is.GreaterThanOrEqualTo(beforeTime));
199+
Assert.That(condition.ConfirmedState.Timestamp, Is.LessThanOrEqualTo(afterTime));
200+
201+
// Verify change masks are cleared
202+
Assert.That(condition.ChangeMasks, Is.EqualTo(NodeStateChangeMasks.None));
203+
Assert.That(condition.ConfirmedState.ChangeMasks, Is.EqualTo(NodeStateChangeMasks.None));
204+
}
205+
206+
/// <summary>
207+
/// Test that SetShelvingState updates the timestamp and clears change masks.
208+
/// </summary>
209+
[Test]
210+
public void SetShelvingStateUpdatesTimestampAndClearsChangeMasks()
211+
{
212+
var alarm = new AlarmConditionState(m_telemetry, null);
213+
alarm.Create(m_context, new NodeId(1), "Alarm", null, true);
214+
alarm.ShelvingState = new ShelvedStateMachineState(alarm);
215+
alarm.ShelvingState.Create(m_context, null, BrowseNames.ShelvingState, null, false);
216+
alarm.ShelvingState.UnshelveTime = new PropertyState<double>(alarm.ShelvingState);
217+
218+
var beforeTime = DateTime.UtcNow;
219+
alarm.SetShelvingState(m_context, true, false, 1000);
220+
var afterTime = DateTime.UtcNow;
221+
222+
// Verify timestamp is updated on UnshelveTime
223+
Assert.That(alarm.ShelvingState.UnshelveTime.Timestamp, Is.GreaterThanOrEqualTo(beforeTime));
224+
Assert.That(alarm.ShelvingState.UnshelveTime.Timestamp, Is.LessThanOrEqualTo(afterTime));
225+
226+
// Verify change masks are cleared
227+
Assert.That(alarm.ChangeMasks, Is.EqualTo(NodeStateChangeMasks.None));
228+
}
229+
230+
/// <summary>
231+
/// Test that subscribed clients are notified when SetActiveState is called.
232+
/// This simulates the issue scenario.
233+
/// </summary>
234+
[Test]
235+
public void SetActiveStateNotifiesSubscribers()
236+
{
237+
var alarm = new AlarmConditionState(m_telemetry, null);
238+
alarm.Create(m_context, new NodeId(1), "Alarm", null, true);
239+
240+
// Initially inactive
241+
alarm.SetActiveState(m_context, false);
242+
var initialTimestamp = alarm.ActiveState.Timestamp;
243+
244+
// Now activate the alarm - timestamp should be greater than or equal to the initial timestamp
245+
// since both could be set to DateTime.UtcNow which has limited precision
246+
var beforeActivation = DateTime.UtcNow;
247+
alarm.SetActiveState(m_context, true);
248+
var afterActivation = DateTime.UtcNow;
249+
250+
// Verify that the timestamp is within the expected range
251+
Assert.That(alarm.ActiveState.Timestamp, Is.GreaterThanOrEqualTo(beforeActivation));
252+
Assert.That(alarm.ActiveState.Timestamp, Is.LessThanOrEqualTo(afterActivation));
253+
254+
// Verify that change masks were cleared (indicating subscribers were notified)
255+
Assert.That(alarm.ActiveState.ChangeMasks, Is.EqualTo(NodeStateChangeMasks.None));
256+
}
257+
67258
/// <summary>
68259
/// Test that UpdateStateAfterEnable calls EvaluateRetainStateOnEnable
69260
/// and that the default implementation sets Retain based on GetRetainState.

0 commit comments

Comments
 (0)