fix(driver-modbus-addressing): resolve Low code-review findings (Driver.Modbus.Addressing-006,007,009)
- Driver.Modbus.Addressing-006: broaden the catch in TryParseFamilyNative so a future helper throwing a non-Argument/Overflow type still satisfies the try-parse contract. - Driver.Modbus.Addressing-007: document that the address grammar does not carry ModbusStringByteOrder (the structured-tag path does); add a 'Grammar scope' bullet to docs/v2/dl205.md. - Driver.Modbus.Addressing-009: reword the ModbusModiconAddress comments so they don't imply a leading-digit invariant the parser doesn't enforce. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -146,4 +146,94 @@ public sealed class ModbusAddressEdgeCaseTests
|
||||
// D0 with a bank base that itself overflows: base 65535 + D1 = 65536.
|
||||
Should.Throw<OverflowException>(() => MelsecAddress.DRegisterToHolding("D1", dBankBase: 65535));
|
||||
}
|
||||
|
||||
// ── TryParse never throws (Driver.Modbus.Addressing-006) ─────────────────────────────────
|
||||
//
|
||||
// The TryParse contract is that it converts every parse failure into a structured (false,
|
||||
// error) return — config-bind hot paths depend on this. The family-native catch was previously
|
||||
// narrow (ArgumentException / OverflowException only); any future helper change that threw a
|
||||
// different exception type (e.g. FormatException from a ushort.Parse swap) would escape as an
|
||||
// unhandled exception out of a TryParse method. These tests assert the defensive contract
|
||||
// across a broad set of pathological inputs.
|
||||
|
||||
[Theory]
|
||||
[InlineData("V")] // V prefix with no digits
|
||||
[InlineData("V99999999999999")] // overflow in user V-memory octal decode
|
||||
[InlineData("V200000")] // overflow in user V-memory octal decode
|
||||
[InlineData("V77777777")] // octal way past 0xFFFF in system bank
|
||||
[InlineData("Y")] // Y prefix with no digits
|
||||
[InlineData("Y8888")] // non-octal digit
|
||||
[InlineData("Y174000")] // octal offset overflows YOutputBaseCoil + value
|
||||
[InlineData("C")] // C prefix alone
|
||||
[InlineData("C99999999")] // overflow in C-relay
|
||||
[InlineData("X")] // X prefix alone
|
||||
[InlineData("X8")] // non-octal digit
|
||||
[InlineData("SP")] // SP prefix alone
|
||||
[InlineData("SP9")] // non-octal digit
|
||||
[InlineData("Z123")] // unknown DL205 prefix
|
||||
public void DL205_TryParse_NeverThrows_ReturnsStructuredError(string addr)
|
||||
{
|
||||
// Defensive contract: any helper failure must surface as (false, non-null error), never
|
||||
// as an unhandled exception out of TryParse.
|
||||
var ok = ModbusAddressParser.TryParse(addr, ModbusFamily.DL205, MelsecFamily.Q_L_iQR, out var result, out var error);
|
||||
ok.ShouldBeFalse();
|
||||
result.ShouldBeNull();
|
||||
error.ShouldNotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("D")] // D prefix alone — no digits
|
||||
[InlineData("D-1")] // negative — would fail ushort.TryParse, must not throw
|
||||
[InlineData("D65536")] // overflow
|
||||
[InlineData("DABC")] // non-decimal digits in D
|
||||
[InlineData("MABC")] // non-decimal digits in M
|
||||
[InlineData("X10000")] // hex overflow (Q-family)
|
||||
[InlineData("XZZZZ")] // non-hex digit (Q-family)
|
||||
[InlineData("Y10000")] // hex overflow (Q-family)
|
||||
public void MELSEC_TryParse_NeverThrows_ReturnsStructuredError(string addr)
|
||||
{
|
||||
var ok = ModbusAddressParser.TryParse(addr, ModbusFamily.MELSEC, MelsecFamily.Q_L_iQR, out var result, out var error);
|
||||
ok.ShouldBeFalse();
|
||||
result.ShouldBeNull();
|
||||
error.ShouldNotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
// ── ModbusStringByteOrder is grammar-out-of-scope (Driver.Modbus.Addressing-007) ────────
|
||||
//
|
||||
// ModbusStringByteOrder (HighByteFirst / LowByteFirst) is the DL205 low-byte-first packing
|
||||
// knob. It is intentionally NOT expressible through the address grammar — there is no token
|
||||
// form to set it and ParsedModbusAddress has no field for it. The string byte order is
|
||||
// configurable only via the structured tag form (ModbusTagDefinition.StringByteOrder), which
|
||||
// is the canonical config path. These tests pin that contract so a future grammar change
|
||||
// can't quietly add a token that conflicts with the array-count slot.
|
||||
|
||||
[Fact]
|
||||
public void Parser_STR_grammar_does_not_carry_StringByteOrder()
|
||||
{
|
||||
// STR20 parses fine — but the result has no StringByteOrder field (the property does
|
||||
// not exist on ParsedModbusAddress). The string byte order must be set on the structured
|
||||
// tag definition, not the grammar string.
|
||||
var ok = ModbusAddressParser.TryParse("40001:STR20", out var result, out _);
|
||||
ok.ShouldBeTrue();
|
||||
result!.DataType.ShouldBe(ModbusDataType.String);
|
||||
result.StringLength.ShouldBe((ushort)20);
|
||||
// Compile-time assertion: ParsedModbusAddress does not expose StringByteOrder.
|
||||
// Searching for a property by reflection would let us assert "no such field":
|
||||
typeof(ParsedModbusAddress)
|
||||
.GetProperty("StringByteOrder")
|
||||
.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parser_rejects_unknown_string_byte_order_token_in_grammar()
|
||||
{
|
||||
// A user trying to express low-byte-first via a grammar suffix like "LOWB" or "HIGH" in
|
||||
// the byte-order slot gets the standard "Unknown byte order" diagnostic — the parser is
|
||||
// explicit that field 3 is the multi-register word/byte order, not the per-string byte
|
||||
// order. The structured tag form is the only configuration path for ModbusStringByteOrder.
|
||||
var ok = ModbusAddressParser.TryParse("40001:STR20:LOWB", out _, out var error);
|
||||
ok.ShouldBeFalse();
|
||||
error.ShouldNotBeNullOrEmpty();
|
||||
error!.ShouldContain("byte order", Case.Insensitive);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user