153 lines
6.2 KiB
C#
153 lines
6.2 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class AbCipBoolInDIntRmwTests
|
|
{
|
|
/// <summary>
|
|
/// Fake tag runtime that stores a DINT value + exposes Read/Write/EncodeValue/DecodeValue
|
|
/// for DInt. RMW tests use one instance as the "parent" runtime (tag name "Motor.Flags")
|
|
/// which the driver's WriteBitInDIntAsync reads + writes.
|
|
/// </summary>
|
|
private sealed class ParentDintFake(AbCipTagCreateParams p) : FakeAbCipTag(p)
|
|
{
|
|
// Uses the base FakeAbCipTag's Value + ReadCount + WriteCount.
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Bit_set_reads_parent_ORs_bit_writes_back()
|
|
{
|
|
var factory = new FakeAbCipTagFactory
|
|
{
|
|
Customise = p => new ParentDintFake(p) { Value = 0b0001 },
|
|
};
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags =
|
|
[
|
|
new AbCipTagDefinition("Flag3", "ab://10.0.0.5/1,0", "Motor.Flags.3", AbCipDataType.Bool),
|
|
],
|
|
Probe = new AbCipProbeOptions { Enabled = false },
|
|
}, "drv-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var results = await drv.WriteAsync(
|
|
[new WriteRequest("Flag3", true)], CancellationToken.None);
|
|
|
|
results.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good);
|
|
|
|
// Parent runtime created under name "Motor.Flags" — distinct from the bit-selector tag.
|
|
factory.Tags.ShouldContainKey("Motor.Flags");
|
|
factory.Tags["Motor.Flags"].Value.ShouldBe(0b1001); // bit 3 set, bit 0 preserved
|
|
factory.Tags["Motor.Flags"].ReadCount.ShouldBe(1);
|
|
factory.Tags["Motor.Flags"].WriteCount.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Bit_clear_preserves_other_bits()
|
|
{
|
|
var factory = new FakeAbCipTagFactory
|
|
{
|
|
Customise = p => new ParentDintFake(p) { Value = unchecked((int)0xFFFFFFFF) },
|
|
};
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags = [new AbCipTagDefinition("F", "ab://10.0.0.5/1,0", "Motor.Flags.3", AbCipDataType.Bool)],
|
|
Probe = new AbCipProbeOptions { Enabled = false },
|
|
}, "drv-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
await drv.WriteAsync([new WriteRequest("F", false)], CancellationToken.None);
|
|
|
|
var updated = Convert.ToInt32(factory.Tags["Motor.Flags"].Value);
|
|
(updated & (1 << 3)).ShouldBe(0); // bit 3 cleared
|
|
(updated & ~(1 << 3)).ShouldBe(unchecked((int)0xFFFFFFF7)); // every other bit preserved
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Concurrent_bit_writes_to_same_parent_compose_correctly()
|
|
{
|
|
var factory = new FakeAbCipTagFactory
|
|
{
|
|
Customise = p => new ParentDintFake(p) { Value = 0 },
|
|
};
|
|
var tags = Enumerable.Range(0, 8)
|
|
.Select(b => new AbCipTagDefinition($"Bit{b}", "ab://10.0.0.5/1,0", $"Flags.{b}", AbCipDataType.Bool))
|
|
.ToArray();
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags = tags,
|
|
Probe = new AbCipProbeOptions { Enabled = false },
|
|
}, "drv-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
await Task.WhenAll(Enumerable.Range(0, 8).Select(b =>
|
|
drv.WriteAsync([new WriteRequest($"Bit{b}", true)], CancellationToken.None)));
|
|
|
|
Convert.ToInt32(factory.Tags["Flags"].Value).ShouldBe(0xFF);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Bit_writes_to_different_parents_each_get_own_runtime()
|
|
{
|
|
var factory = new FakeAbCipTagFactory
|
|
{
|
|
Customise = p => new ParentDintFake(p) { Value = 0 },
|
|
};
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags =
|
|
[
|
|
new AbCipTagDefinition("A", "ab://10.0.0.5/1,0", "Motor1.Flags.0", AbCipDataType.Bool),
|
|
new AbCipTagDefinition("B", "ab://10.0.0.5/1,0", "Motor2.Flags.0", AbCipDataType.Bool),
|
|
],
|
|
Probe = new AbCipProbeOptions { Enabled = false },
|
|
}, "drv-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
await drv.WriteAsync([new WriteRequest("A", true)], CancellationToken.None);
|
|
await drv.WriteAsync([new WriteRequest("B", true)], CancellationToken.None);
|
|
|
|
factory.Tags.ShouldContainKey("Motor1.Flags");
|
|
factory.Tags.ShouldContainKey("Motor2.Flags");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Repeat_bit_writes_reuse_one_parent_runtime()
|
|
{
|
|
var factory = new FakeAbCipTagFactory
|
|
{
|
|
Customise = p => new ParentDintFake(p) { Value = 0 },
|
|
};
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags =
|
|
[
|
|
new AbCipTagDefinition("Bit0", "ab://10.0.0.5/1,0", "Flags.0", AbCipDataType.Bool),
|
|
new AbCipTagDefinition("Bit5", "ab://10.0.0.5/1,0", "Flags.5", AbCipDataType.Bool),
|
|
],
|
|
Probe = new AbCipProbeOptions { Enabled = false },
|
|
}, "drv-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
await drv.WriteAsync([new WriteRequest("Bit0", true)], CancellationToken.None);
|
|
await drv.WriteAsync([new WriteRequest("Bit5", true)], CancellationToken.None);
|
|
|
|
// Three factory invocations: two bit-selector tags (never used for writes, but the
|
|
// driver may create them opportunistically) + one shared parent. Assert the parent was
|
|
// init'd exactly once + used for both writes.
|
|
factory.Tags["Flags"].InitializeCount.ShouldBe(1);
|
|
factory.Tags["Flags"].WriteCount.ShouldBe(2);
|
|
Convert.ToInt32(factory.Tags["Flags"].Value).ShouldBe(0x21); // bits 0 + 5
|
|
}
|
|
}
|