Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/ScriptedS7Plc.cs
T
Joseph Doherty 87a7414df7 fix(s7): refuse unservable tag shapes per tag at every gate (R07:H3, R07:U4)
The init-time tag guards THREW: one mis-authored tag (DB1.DBZ0, DB1.DBW0 typed
Float64, T5 typed Int32, a writable Bool array) faulted the whole driver and the
actor retried the same init every 10 s forever, darkening every tag on the PLC.
Neither the editor nor the deploy gate checked any of it.

- S7AddressParser moves to S7.Contracts (same namespace) and a new
  S7TagShapeRules there is the single authority on servable shapes.
- S7TagDefinitionFactory.FromTagConfig applies it, so the AdminUI editor, the
  deploy gate (Inspect, with the specific reason) and the driver init refuse
  the same tags. Init now skips a refused tag (ISkippedTagReporter) instead of
  throwing; the throwing RejectUnsupportedTagConfigs is gone.
- New rules on top of the old guards: a narrow type at the wrong access width
  (every read failed) and a Bool array not starting at bit 0 (U4: DB1.DBX0.3
  silently read bit 0 of bytes 0..n).
- AdminUI test fixtures that used addresses the driver could never serve
  (DB1.DBS0, a browse "SomeAddress", Int32/Float32 on DBW) now use real ones.

Refs #742
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 15:04:19 -04:00

143 lines
5.4 KiB
C#

using S7DataItem = global::S7.Net.Types.DataItem;
using S7NetDataType = global::S7.Net.DataType;
using VarType = global::S7.Net.VarType;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests;
/// <summary>
/// A scriptable <see cref="IS7PlcFactory"/>: every connection it creates delegates its wire
/// operations to the factory's handlers, so a test scripts the device once and observes every
/// connection (including reopened ones). Counts wire operations so a test can prove what did
/// NOT go to the wire without timing anything.
/// </summary>
internal sealed class ScriptedS7PlcFactory : IS7PlcFactory
{
private int _wireOps;
/// <summary>Every connection created, in order.</summary>
public List<ScriptedS7Plc> Created { get; } = [];
/// <summary>Wire operations issued (reads, multi-var reads, writes; not opens or status probes).</summary>
public int WireOps => Volatile.Read(ref _wireOps);
/// <summary>Hook run inside every <see cref="IS7Plc.OpenAsync"/>; the default opens immediately.</summary>
public Func<int, CancellationToken, Task> Open { get; set; } = (_, _) => Task.CompletedTask;
/// <summary>Per-address string read; the default returns <c>(ushort)42</c>.</summary>
public Func<string, CancellationToken, Task<object?>> Read { get; set; } = (_, _) => Task.FromResult<object?>((ushort)42);
/// <summary>Block read; the default returns <c>count</c> zero bytes.</summary>
public Func<S7NetDataType, int, int, int, CancellationToken, Task<byte[]>> ReadBytes { get; set; }
= (_, _, _, count, _) => Task.FromResult(new byte[count]);
/// <summary>Multi-var read; the default answers every item with a zero of its var type.</summary>
public Func<List<S7DataItem>, CancellationToken, Task<IReadOnlyList<S7DataItem>>> ReadMulti { get; set; }
= (items, _) =>
{
foreach (var i in items)
i.Value = i.VarType switch
{
VarType.Bit => false,
VarType.Byte => (byte)0,
VarType.Word => (ushort)0,
_ => (uint)0,
};
return Task.FromResult<IReadOnlyList<S7DataItem>>(items);
};
/// <summary>Per-address string write; the default succeeds.</summary>
public Func<string, object, CancellationToken, Task> Write { get; set; } = (_, _, _) => Task.CompletedTask;
/// <summary>Block write; the default succeeds.</summary>
public Func<S7NetDataType, int, int, byte[], CancellationToken, Task> WriteBytes { get; set; }
= (_, _, _, _, _) => Task.CompletedTask;
/// <summary>CPU-status probe; the default answers.</summary>
public Func<CancellationToken, Task> ReadStatus { get; set; } = _ => Task.CompletedTask;
/// <inheritdoc />
public IS7Plc Create(S7CpuType cpuType, string host, int port, short rack, short slot, TimeSpan timeout)
{
lock (Created)
{
var plc = new ScriptedS7Plc(this, Created.Count);
Created.Add(plc);
return plc;
}
}
/// <summary>Counts one wire operation.</summary>
internal void CountWireOp() => Interlocked.Increment(ref _wireOps);
}
/// <summary>One connection created by <see cref="ScriptedS7PlcFactory"/>.</summary>
internal sealed class ScriptedS7Plc(ScriptedS7PlcFactory factory, int index) : IS7Plc
{
/// <summary>Zero-based creation index.</summary>
public int Index { get; } = index;
/// <inheritdoc />
public bool IsConnected { get; private set; }
/// <summary>True once disposed.</summary>
public bool Disposed { get; private set; }
/// <inheritdoc />
public int MaxPduSize => 240;
/// <inheritdoc />
public async Task OpenAsync(CancellationToken cancellationToken)
{
await factory.Open(Index, cancellationToken).ConfigureAwait(false);
IsConnected = true;
}
/// <inheritdoc />
public Task<object?> ReadAsync(string address, CancellationToken cancellationToken)
{
factory.CountWireOp();
return factory.Read(address, cancellationToken);
}
/// <inheritdoc />
public Task<byte[]> ReadBytesAsync(S7NetDataType area, int db, int startByteAdr, int count, CancellationToken cancellationToken)
{
factory.CountWireOp();
return factory.ReadBytes(area, db, startByteAdr, count, cancellationToken);
}
/// <inheritdoc />
public Task<IReadOnlyList<S7DataItem>> ReadMultipleVarsAsync(List<S7DataItem> items, CancellationToken cancellationToken)
{
factory.CountWireOp();
return factory.ReadMulti(items, cancellationToken);
}
/// <inheritdoc />
public Task WriteAsync(string address, object value, CancellationToken cancellationToken)
{
factory.CountWireOp();
return factory.Write(address, value, cancellationToken);
}
/// <inheritdoc />
public Task WriteBytesAsync(S7NetDataType area, int db, int startByteAdr, byte[] value, CancellationToken cancellationToken)
{
factory.CountWireOp();
return factory.WriteBytes(area, db, startByteAdr, value, cancellationToken);
}
/// <inheritdoc />
public Task ReadStatusAsync(CancellationToken cancellationToken) => factory.ReadStatus(cancellationToken);
/// <inheritdoc />
public void Close() => IsConnected = false;
/// <inheritdoc />
public void Dispose()
{
Disposed = true;
IsConnected = false;
}
}