Files
lmxopcua/code-reviews/Driver.S7.Cli/findings.md
Joseph Doherty 67ef6c4ebc fix(driver-s7-cli): resolve Low code-review findings (Driver.S7.Cli-004,005,006,007)
- Driver.S7.Cli-004: 'await using var driver' is the sole driver
  disposal path; dropped the redundant explicit await ShutdownAsync from
  each command's finally.
- Driver.S7.Cli-005: deleted the stale empty
  tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests/ directory (the real test
  project lives under tests/Drivers/Cli/).
- Driver.S7.Cli-006: S7CommandBaseBuildOptionsTests cover the probe
  toggle, timeout mapping, host/port/CPU/rack/slot wiring, and tag list
  passthrough.
- Driver.S7.Cli-007: re-added the SubscribeCommand handler comment
  explaining the CliFx IConsole.Output usage and that the poll-thread
  raises events.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 08:34:48 -04:00

12 KiB

Code Review — Driver.S7.Cli

Field Value
Module src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli
Reviewer Claude Code
Review date 2026-05-22
Commit reviewed 76d35d1
Status Reviewed
Open findings 0

Checklist coverage

A comprehensive review completes every category, recording "No issues found" where a category produced nothing rather than leaving it blank.

# Category Result
1 Correctness & logic bugs Driver.S7.Cli-001, Driver.S7.Cli-002
2 OtOpcUa conventions No issues found
3 Concurrency & thread safety No issues found
4 Error handling & resilience Driver.S7.Cli-001, Driver.S7.Cli-003
5 Security No issues found
6 Performance & resource management Driver.S7.Cli-004
7 Design-document adherence Driver.S7.Cli-002
8 Code organization & conventions Driver.S7.Cli-005
9 Testing coverage Driver.S7.Cli-006
10 Documentation & comments Driver.S7.Cli-002, Driver.S7.Cli-007

Findings

Driver.S7.Cli-001

Field Value
Severity Medium
Category Error handling & resilience
Location src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/WriteCommand.cs:65-80
Status Resolved

Description: WriteCommand.ParseValue parses numeric and DateTime values with the raw BCL parsers (short.Parse, float.Parse, DateTime.Parse, etc.). On malformed input these throw FormatException / OverflowException, which are not CliFx.Exceptions.CommandException. CliFx renders a CommandException as a clean one-line error with a non-zero exit code, but renders any other exception as a full .NET stack trace. The ParseValue bool path is handled correctly (it throws CommandException for unrecognised input), so the command is internally inconsistent: write -t Bool -v maybe gives a friendly message while write -t Int16 -v xyz dumps a stack trace. The module own test ParseValue_non_numeric_for_numeric_types_throws asserts the raw FormatException leaks, confirming the behaviour is unintended-but-shipped.

Recommendation: Wrap the numeric / DateTime parses in a try/catch that re-throws FormatException and OverflowException as CliFx.Exceptions.CommandException with a message that names the --type and the offending value — matching the bool path. Update the test to expect CommandException.

Resolution: Resolved 2026-05-22 — wrapped all numeric/DateTime BCL parses in try/catch(FormatException) and try/catch(OverflowException) that re-throw as CommandException with a message naming the --type and the offending value; updated ParseValue_non_numeric_for_numeric_types_throws to assert CommandException, and added an overflow-edge test.

Driver.S7.Cli-002

Field Value
Severity Medium
Category Design-document adherence
Location src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/ReadCommand.cs:22-29, Commands/WriteCommand.cs:21-33, Commands/SubscribeCommand.cs:18-21; docs/Driver.S7.Cli.md:70-73,80-81
Status Resolved

Description: The --type option help text on read, write, and subscribe advertises the full S7DataType set (Int64 / UInt64 / Float64 / String / DateTime), and docs/Driver.S7.Cli.md shows a worked read ... -t String --string-length 80 example plus a --string-length flag on read/write. The underlying S7Driver (S7Driver.cs:241-245 for reads, :316-320 for writes) throws NotSupportedException for Int64, UInt64, Float64, String, and DateTime — the driver maps that to BadNotSupported. Consequently every CLI invocation using one of those types — and the documented --string-length string-read example — fails at runtime with 0x803D0000 (Bad). The CLI surface and docs promise capability the driver does not yet implement.

Recommendation: Either (a) trim the --type help text and the --string-length flag/examples to the implemented set (Bool / Byte / Int16 / UInt16 / Int32 / UInt32 / Float32) until the follow-up driver PR lands, or (b) keep the surface but add a one-line "types beyond Float32 are not yet implemented and surface BadNotSupported" caveat to the help text and docs/Driver.S7.Cli.md. Option (a) is preferred so the CLI does not offer options that cannot succeed.

Resolution: Resolved 2026-05-22 — updated the --type help text on read, write, and subscribe to list the implemented set (Bool/Byte/Int16/UInt16/Int32/UInt32/Float32) and appended a one-line caveat that Int64/UInt64/Float64/String/DateTime are not yet implemented and will return BadNotSupported.

Driver.S7.Cli-003

Field Value
Severity Medium
Category Error handling & resilience
Location src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/ProbeCommand.cs:38-50
Status Resolved

Description: ProbeCommand XML doc and the Driver.S7.Cli.md "fastest is the device talking" framing say the probe "connects ... prints health" and "surfaces BadNotSupported" when PUT/GET is disabled. But when the PLC is unreachable (connection refused, host down, wrong slot), driver.InitializeAsync throws and the exception propagates straight out of ExecuteAsync — the code that prints Host:, Health:, Last error:, and the snapshot is never reached. The most common probe failure (device not reachable at all) therefore produces a CliFx stack trace rather than the structured health report the command exists to give. Note PUT/GET-disabled only surfaces during ReadAsync (after a successful connect), so that one path does reach the health print — but a refused TCP connect does not.

Recommendation: Wrap the InitializeAsync + ReadAsync body in a try/catch that, on failure, still prints the Host: / CPU: lines and a Health: / Last error: report derived from driver.GetHealth() (which InitializeAsync sets to Faulted with the exception message before re-throwing). The probe should report an unreachable device, not crash on it.

Resolution: Resolved 2026-05-22 — wrapped the InitializeAsync + ReadAsync body in a try/catch that on any non-cancellation failure still prints the structured Host:, CPU:, Health:, and Last error: lines derived from driver.GetHealth(), so an unreachable device produces a health report rather than a stack trace.

Driver.S7.Cli-004

Field Value
Severity Low
Category Performance & resource management
Location src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/ProbeCommand.cs:36,53, Commands/ReadCommand.cs:45,54, Commands/WriteCommand.cs:51,60, Commands/SubscribeCommand.cs:39,73
Status Resolved

Description: Every command declares the driver with await using var driver = new S7Driver(...) and also calls await driver.ShutdownAsync(...) in a finally block. S7Driver.DisposeAsync itself calls ShutdownAsync, so shutdown runs twice per command (three times for subscribe, which also unsubscribes). ShutdownAsync is idempotent (Plc?.Close() is best-effort, _subscriptions is cleared) so there is no functional bug, but the explicit finally-block ShutdownAsync call is redundant given the await using. It is also slightly misleading — a reader may assume the await using is not actually disposing.

Recommendation: Drop the explicit await driver.ShutdownAsync(...) from the finally blocks and rely on await using for teardown; keep only the subscribe command UnsubscribeAsync. Alternatively drop await using and keep the explicit finally. Pick one disposal mechanism per command.

Resolution: Resolved 2026-05-23 — dropped the explicit await driver.ShutdownAsync(CancellationToken.None) calls from the finally blocks of ProbeCommand, ReadCommand, WriteCommand, and SubscribeCommand; await using is now the sole driver-disposal mechanism per command (DisposeAsync itself runs ShutdownAsync), and the subscribe command keeps UnsubscribeAsync in its finally because that is a subscription-lifecycle concern, not driver disposal. Added CommandDisposalConventionsTests to guard the source-level convention against regression.

Driver.S7.Cli-005

Field Value
Severity Low
Category Code organization & conventions
Location tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests/
Status Resolved

Description: A stale directory tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests/ exists containing only an obj/ folder — no .csproj, no source. The real test project lives at tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests/. The empty directory is a leftover from the project move into tests/Drivers/Cli/ and is not referenced by ZB.MOM.WW.OtOpcUa.slnx. It is dead clutter that can mislead anyone grepping the tree for the S7 CLI test project.

Recommendation: Delete the stale tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests/ directory (including its obj/). This is outside the module src/ tree but is the S7 CLI own orphaned test folder, so it belongs to this module cleanup.

Resolution: Resolved 2026-05-23 — deleted the stale tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests/ directory (only contained a leftover obj/ from before the move into tests/Drivers/Cli/; no tracked files). The real test project at tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests/ is untouched.

Driver.S7.Cli-006

Field Value
Severity Low
Category Testing coverage
Location tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests/WriteCommandParseValueTests.cs
Status Resolved

Description: The only test file covers WriteCommand.ParseValue and ReadCommand.SynthesiseTagName. S7CommandBase.BuildOptions — which maps the host / port / CPU / rack / slot / timeout flags onto an S7DriverOptions and forces Probe.Enabled = false — has no test, despite being pure, deterministic, and internal-visible to the test assembly via InternalsVisibleTo. A regression that dropped Probe = new S7ProbeOptions { Enabled = false } (which would start an unwanted background probe loop in a one-shot CLI run) or mis-mapped TimeoutMs would not be caught. ParseValue is also missing an explicit overflow-edge test (e.g. Byte value 256) — the current ParseValue_Byte_ranges test stops at 255.

Recommendation: Add a BuildOptions test (assert Probe.Enabled == false, Timeout matches TimeoutMs, and host/port/CPU/rack/slot flow through). Add an overflow case to the ParseValue numeric tests once Driver.S7.Cli-001 is resolved so the test asserts the wrapped CommandException.

Resolution: Resolved 2026-05-23 — added S7CommandBaseBuildOptionsTests covering Probe.Enabled=false (one-shot CLI guarantee), TimeoutMs→Timeout TimeSpan mapping, host/port/CPU/rack/slot flowing through, and tag-list passthrough. The overflow-edge ParseValue test was already added under Driver.S7.Cli-001 (ParseValue_overflow_for_numeric_types_throws_CommandException).

Driver.S7.Cli-007

Field Value
Severity Low
Category Documentation & comments
Location src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/SubscribeCommand.cs:45-51
Status Resolved

Description: The Modbus CLI SubscribeCommand carries an explanatory comment on the OnDataChange handler ("Route every data-change event to the CliFx console (not System.Console — the analyzer flags it + IConsole is the testable abstraction)"). The S7 SubscribeCommand is a near-verbatim copy but dropped that comment, so the non-obvious reason the handler uses console.Output.WriteLine (synchronous, on a driver background thread) instead of System.Console or the async WriteLineAsync is undocumented here. Minor, but the rationale is worth keeping consistent across the CLI family.

Recommendation: Re-add the one-line comment from the Modbus SubscribeCommand so the S7 copy explains why the event handler writes via console.Output synchronously.

Resolution: Resolved 2026-05-23 — re-added the explanatory comment above the OnDataChange handler in the S7 SubscribeCommand, mirroring the Modbus copy: it explains the use of the CliFx IConsole.Output abstraction (rather than System.Console) and notes that the handler runs synchronously because it's raised from a driver background thread. Added SubscribeCommandConsoleHandlerCommentTests to guard the rationale against future copy-paste regressions.