098adf43d0
DisposeRuntimes() now disposes and clears _rmwLocks, _creationLocks, and _runtimeLocks so ReinitializeAsync/ShutdownAsync cycles don't orphan their SemaphoreSlim instances. Mirrors the TwinCAT _bitRmwLocks fix already shipped.
258 lines
11 KiB
C#
258 lines
11 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class AbLegacyBitRmwTests
|
|
{
|
|
/// <summary>Verifies that setting a bit reads the parent word, ORs the bit, and writes back.</summary>
|
|
[Fact]
|
|
public async Task Bit_set_reads_parent_word_ORs_bit_writes_back()
|
|
{
|
|
var factory = new FakeAbLegacyTagFactory
|
|
{
|
|
Customise = p => new FakeAbLegacyTag(p) { Value = (short)0b0001 },
|
|
};
|
|
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
|
{
|
|
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags = [new AbLegacyTagDefinition("Flag3", "ab://10.0.0.5/1,0", "N7:0/3", AbLegacyDataType.Bit)],
|
|
Probe = new AbLegacyProbeOptions { 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(AbLegacyStatusMapper.Good);
|
|
factory.Tags.ShouldContainKey("N7:0"); // parent word runtime created
|
|
Convert.ToInt32(factory.Tags["N7:0"].Value).ShouldBe(0b1001);
|
|
}
|
|
|
|
/// <summary>Verifies that clearing a bit preserves other bits in the word.</summary>
|
|
[Fact]
|
|
public async Task Bit_clear_preserves_other_bits_in_N_file_word()
|
|
{
|
|
var factory = new FakeAbLegacyTagFactory
|
|
{
|
|
Customise = p => new FakeAbLegacyTag(p) { Value = unchecked((short)0xFFFF) },
|
|
};
|
|
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
|
{
|
|
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags = [new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", "N7:0/3", AbLegacyDataType.Bit)],
|
|
Probe = new AbLegacyProbeOptions { Enabled = false },
|
|
}, "drv-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
await drv.WriteAsync([new WriteRequest("F", false)], CancellationToken.None);
|
|
|
|
Convert.ToInt32(factory.Tags["N7:0"].Value).ShouldBe(unchecked((short)0xFFF7));
|
|
}
|
|
|
|
/// <summary>Verifies that concurrent bit writes to the same word compose correctly.</summary>
|
|
[Fact]
|
|
public async Task Concurrent_bit_writes_to_same_word_compose_correctly()
|
|
{
|
|
var factory = new FakeAbLegacyTagFactory
|
|
{
|
|
Customise = p => new FakeAbLegacyTag(p) { Value = (short)0 },
|
|
};
|
|
var tags = Enumerable.Range(0, 8)
|
|
.Select(b => new AbLegacyTagDefinition($"Bit{b}", "ab://10.0.0.5/1,0", $"N7:0/{b}", AbLegacyDataType.Bit))
|
|
.ToArray();
|
|
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
|
{
|
|
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags = tags,
|
|
Probe = new AbLegacyProbeOptions { 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["N7:0"].Value).ShouldBe(0xFF);
|
|
}
|
|
|
|
/// <summary>Verifies that repeated bit writes reuse the parent word runtime.</summary>
|
|
[Fact]
|
|
public async Task Repeat_bit_writes_reuse_parent_runtime()
|
|
{
|
|
var factory = new FakeAbLegacyTagFactory
|
|
{
|
|
Customise = p => new FakeAbLegacyTag(p) { Value = (short)0 },
|
|
};
|
|
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
|
{
|
|
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags =
|
|
[
|
|
new AbLegacyTagDefinition("Bit0", "ab://10.0.0.5/1,0", "N7:0/0", AbLegacyDataType.Bit),
|
|
new AbLegacyTagDefinition("Bit5", "ab://10.0.0.5/1,0", "N7:0/5", AbLegacyDataType.Bit),
|
|
],
|
|
Probe = new AbLegacyProbeOptions { 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);
|
|
|
|
factory.Tags["N7:0"].InitializeCount.ShouldBe(1);
|
|
factory.Tags["N7:0"].WriteCount.ShouldBe(2);
|
|
Convert.ToInt32(factory.Tags["N7:0"].Value).ShouldBe(0x21); // bits 0 + 5
|
|
}
|
|
|
|
/// <summary>B/I/O-file bit set reads the parent word, ORs the bit, writes it back (was BadNotSupported).</summary>
|
|
[Theory]
|
|
[InlineData("B3:0/3", "B3:0")]
|
|
[InlineData("I:0/3", "I:0")]
|
|
[InlineData("O:1/3", "O:1")]
|
|
public async Task Bit_set_on_B_I_O_file_RMWs_parent_word(string bitAddr, string parentName)
|
|
{
|
|
var factory = new FakeAbLegacyTagFactory
|
|
{
|
|
Customise = p => new FakeAbLegacyTag(p) { Value = (short)0b0001 },
|
|
};
|
|
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
|
{
|
|
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags = [new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", bitAddr, AbLegacyDataType.Bit)],
|
|
Probe = new AbLegacyProbeOptions { Enabled = false },
|
|
}, "drv-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var results = await drv.WriteAsync([new WriteRequest("F", true)], CancellationToken.None);
|
|
|
|
results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
|
factory.Tags.ShouldContainKey(parentName);
|
|
Convert.ToInt32(factory.Tags[parentName].Value).ShouldBe(0b1001);
|
|
}
|
|
|
|
/// <summary>B-file bit clear preserves the other bits in the parent word.</summary>
|
|
[Fact]
|
|
public async Task Bit_clear_preserves_other_bits_in_B_file_word()
|
|
{
|
|
var factory = new FakeAbLegacyTagFactory
|
|
{
|
|
Customise = p => new FakeAbLegacyTag(p) { Value = unchecked((short)0xFFFF) },
|
|
};
|
|
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
|
{
|
|
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags = [new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", "B3:0/3", AbLegacyDataType.Bit)],
|
|
Probe = new AbLegacyProbeOptions { Enabled = false },
|
|
}, "drv-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
await drv.WriteAsync([new WriteRequest("F", false)], CancellationToken.None);
|
|
|
|
Convert.ToInt32(factory.Tags["B3:0"].Value).ShouldBe(unchecked((short)0xFFF7));
|
|
}
|
|
|
|
/// <summary>Concurrent bit writes to the same B-file word compose correctly (per-parent lock).</summary>
|
|
[Fact]
|
|
public async Task Concurrent_bit_writes_to_same_B_file_word_compose_correctly()
|
|
{
|
|
var factory = new FakeAbLegacyTagFactory
|
|
{
|
|
Customise = p => new FakeAbLegacyTag(p) { Value = (short)0 },
|
|
};
|
|
var tags = Enumerable.Range(0, 8)
|
|
.Select(b => new AbLegacyTagDefinition($"Bit{b}", "ab://10.0.0.5/1,0", $"B3:0/{b}", AbLegacyDataType.Bit))
|
|
.ToArray();
|
|
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
|
{
|
|
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags = tags,
|
|
Probe = new AbLegacyProbeOptions { 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["B3:0"].Value).ShouldBe(0xFF);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that disposing (teardown) and re-initializing the driver does not orphan the
|
|
/// per-parent RMW-lock semaphores from the previous session. A bit write after the
|
|
/// reinit cycle must succeed, proving that <see cref="DeviceState.DisposeRuntimes"/> clears
|
|
/// the lock map so fresh semaphores are created for the new session rather than re-using
|
|
/// (already-disposed) handles from the old one.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task RMW_locks_are_disposed_and_fresh_after_driver_reinitialise()
|
|
{
|
|
var factory = new FakeAbLegacyTagFactory
|
|
{
|
|
Customise = p => new FakeAbLegacyTag(p) { Value = (short)0 },
|
|
};
|
|
var opts = new AbLegacyDriverOptions
|
|
{
|
|
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags = [new AbLegacyTagDefinition("Bit2", "ab://10.0.0.5/1,0", "N7:0/2", AbLegacyDataType.Bit)],
|
|
Probe = new AbLegacyProbeOptions { Enabled = false },
|
|
};
|
|
|
|
// First session: init + bit write — seeds an RMW lock entry for "N7:0".
|
|
var drv = new AbLegacyDriver(opts, "drv-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
var firstResult = await drv.WriteAsync([new WriteRequest("Bit2", true)], CancellationToken.None);
|
|
firstResult.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
|
|
|
// Teardown — DisposeRuntimes must dispose + clear the RMW lock map.
|
|
await drv.DisposeAsync();
|
|
|
|
// Second session: re-init on a fresh driver instance (mirrors ReinitializeAsync behavior).
|
|
factory.Tags.Clear();
|
|
var drv2 = new AbLegacyDriver(opts, "drv-1", factory);
|
|
await drv2.InitializeAsync("{}", CancellationToken.None);
|
|
var secondResult = await drv2.WriteAsync([new WriteRequest("Bit2", false)], CancellationToken.None);
|
|
|
|
// Must succeed — a disposed/orphaned semaphore would throw ObjectDisposedException.
|
|
secondResult.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
|
Convert.ToInt32(factory.Tags["N7:0"].Value).ShouldBe(0);
|
|
|
|
await drv2.DisposeAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// A non-zero libplctag status returned by the parent-word write is surfaced as a non-Good
|
|
/// OPC UA StatusCode — the PCCC device rejection propagates to the caller.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Bit_write_surfaces_device_rejection_status()
|
|
{
|
|
// Arrange: the parent tag reads OK (Status = 0) but write returns a timeout error.
|
|
const int errorTimeout = (int)libplctag.Status.ErrorTimeout;
|
|
var factory = new FakeAbLegacyTagFactory
|
|
{
|
|
Customise = p => new FakeAbLegacyTag(p)
|
|
{
|
|
Value = (short)0b0001,
|
|
WriteStatusOverride = errorTimeout,
|
|
},
|
|
};
|
|
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
|
{
|
|
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags = [new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", "I:0/3", AbLegacyDataType.Bit)],
|
|
Probe = new AbLegacyProbeOptions { Enabled = false },
|
|
}, "drv-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
// Act
|
|
var results = await drv.WriteAsync([new WriteRequest("F", true)], CancellationToken.None);
|
|
|
|
// Assert: the write rejection must NOT be silently swallowed as Good.
|
|
var statusCode = results.Single().StatusCode;
|
|
statusCode.ShouldNotBe(AbLegacyStatusMapper.Good);
|
|
statusCode.ShouldBe(AbLegacyStatusMapper.MapLibplctagStatus(errorTimeout));
|
|
}
|
|
}
|