fix(common): coerce null/empty/whitespace observation Result to UNAVAILABLE#193
fix(common): coerce null/empty/whitespace observation Result to UNAVAILABLE#193ottobolyos wants to merge 12 commits into
Conversation
66db93a to
0a58cfb
Compare
a2c41aa to
a90e23b
Compare
a90e23b to
9d1e172
Compare
|
@PatrickRitchie, this PR fixes a Part 1 spec violation that touches every wire output: |
|
@PatrickRitchie — heads-up: while validating PR #189's dime-connector build I caught an unrelated multi-TFM Release-pack regression introduced by an earlier warnings sweep. The minimal fix lands in PR #194 (just opened as draft); the broader warnings cleanup plus a Release-pack CI gate ships in PR #195. |
UpdatesI updated the PR to allow this feature to be toggled on/off based on the 'InputValidationLevel = strict' configuration parameter. This should allow any existing implementation to still work while allowing a strict adherence to the standard when required. I also added a separate DeviceValidationLevel configuration parameter in order to allow a 'non-standard' DataItem or Component to work with the 'strict' InputValidationLevel. This is common for end users to use a DataItem with a custom Type or SubType. TestsI'm not sure extactly how this affects the tests but I saw where InputValidationLevel is used a few times so that may be why it was failing. |
a61a364 to
6ab2685
Compare
MTConnectHttpServer.cs moved in the v7.0-prerelease landings on 2026-06-26 (Patrick's direct pushes after PR TrakHound#201 merged). Every route handler's line-number reference in docs/reference/http-api.md shifted by 4-22 lines. The committed page still pointed at pre-move lines, so `docs/scripts/generate-reference.sh --check` failed on every CI run since 2026-06-26 - blocking the whole Docs-site + build-and-test + route-check-e2e job DAG downstream. Regenerated http-api.md against current source. Line-number metadata only; no content changes.
…rrors The `Prepare generated docs` job runs `generate-api-ref.sh` after the drift gate; that script builds `build/MTConnect.NET-SysML-Import` which transitively references Scriban 7.2.0. NuGet detects three known vulnerabilities in that version: - GHSA-6q7j-xr26-3h2c (moderate, NU1902) - GHSA-7jvp-hj45-2f2m (high, NU1903) - GHSA-q6rr-fm2g-g5x8 (moderate, NU1902) `<TreatWarningsAsErrors>` promotes them to errors, so the job fails on `build/MTConnect.NET-SysML-Import.csproj`. Bumping to 7.2.2 (per dependabot PR TrakHound#204) resolves all three advisories with no source changes required. Piggybacked into this drift-regen PR because the two failures share the same failing job (`Prepare generated docs`) and land as the same gate red across every push and every PR against master. Landing them together turns the whole DAG green in a single merge; landing them separately leaves the gate red across the intermediate state. Supersedes dependabot TrakHound#204.
… decision Commit c2041ce ("Updated Docs Site", 2026-06-26) reverted the nav logo to a plain-text title on the grounds that "logo might be too small to read". The `themeConfig.logo` and `themeConfig.siteTitle: false` lines in `docs/.vitepress/config.ts` were commented out; VitePress now renders "MTConnect.NET" as text in `.VPNavBarTitle` rather than an <img> under the previous logo-in-nav layout. The Landing_Page_Carries_The_House_Style_Surfaces fixture has been red on every push to master since 2026-06-26 because it asserted the old logo-in-nav layout that the maintainer removed. Adapting the fixture to the current maintainer intent: assert the nav-bar title text is "MTConnect.NET" instead of the img.logo `src`. The hero image assertion (line 349-352) continues to pin the wordmark rendering in the landing hero block, which the maintainer explicitly retained (same commit: "Updated hero logo to use a larger image").
Commit c2041ce ("Updated Docs Site" 2026-06-26) swapped the hero image from `/logo.png` (32-px favicon) to `/logo-large.png` (larger asset) with the message "Updated hero logo to use a larger image". The Landing_Page_Carries_The_House_Style_Surfaces fixture asserted `Does.EndWith("/logo.png")` against the hero-image src, which was correct for the old asset but fails for the new larger asset. Loosen the assertion to `Does.Match(@"/logo[^/]*\.png$")` so any `/logo*.png` variant satisfies it. Survives future asset swaps in the same shape (e.g. `/logo-xl.png`, `/logo-dark.png`) without another test edit. The nav-title, favicon, and social-card assertions continue to pin the specific `/logo.png` path.
This is often used for messages and program names. SHDR should pass the value as is to the Agent and the Agent should then decide (based on validation level) whether to accept the value or not.
Although, based on the MTConnect Standard, a value should never be 'empty', an 'empty' value should be able to be accepted as many adpaters/agents don't adhere to this restriction and could cause issues with real world implementations. An InputValidationLevel set to 'strict' should rewrite an 'empty' value as 'UNAVAILABLE' so that it strictly conforms to the standard.
…nectDevices validation. This allows a device to be validated at a different level than observations/assets
6ab2685 to
b788148
Compare
The new `DeviceValidationLevel` property + enum added on this branch adds one config key row to `IAgentConfiguration` and `AgentConfiguration`; the drift gate `docs/scripts/generate-reference.sh --check` reports DRIFT until the generated `docs/reference/configuration.md` catches up. Runs the generator to produce the current output.
Summary
MTConnectAgent.AddObservationaccepted observations whose Result value wasnull, the empty string"", or whitespace-only and forwarded that value verbatim onto every wire transport (HTTP/current,/sample, MQTT JSON-cppagent, SHDR). MTConnect Part 1 §Observation Information Model — Representation — Observation Values mandates"UNAVAILABLE"as the sole valid representation of a missing or undetermined observation value:The empty string is, by definition, not a Valid Data Value (it parses as no value at all under every typed DataItem schema). This PR adds an unconditional coerce step in the canonical
AddObservation(string deviceKey, IObservationInput, …)overload — null / empty / whitespace Result is rewritten toObservation.Unavailablebefore validation, so:Ignore/Warning/Remove: publishesUNAVAILABLErather than the empty value, satisfying the spec mandate on the wire.Strict: coerces toUNAVAILABLE, the observation lands rather than being silently dropped.The coerce skips CONDITION observations (their Level enum cannot carry the empty pathology). DATA_SET / TABLE / TIME_SERIES Representation pre-fill of Count / SampleCount runs after the coerce — semantics for those representations are unchanged.
No opt-out flag is exposed: the spec mandate is
MUST, so a way to opt out of compliance would itself be non-conformant.Behaviour change
null""(empty)"UNAVAILABLE""""""UNAVAILABLE"" "(whitespace)" ""UNAVAILABLE""AVAILABLE"(concrete)"AVAILABLE""AVAILABLE"(unchanged)""underStrict"UNAVAILABLE"Tests
Unit (NUnit,
tests/MTConnect.NET-Common-Tests/Agents/AddObservationEmptyResultCoerceTests.cs, 10 cases):InputValidationLevelnull/""/ whitespace family underWarningStrict(lands, not silently dropped)Wire-level E2E (xUnit,
[Trait("Category", "E2E")]):tests/MTConnect.NET-Integration-Tests/Workflows/AddObservationEmptyResultUnavailableWorkflowTests.cs— HTTP/current:UNAVAILABLE(6-case[Theory])UNAVAILABLE)Strictvalidation → AVAILABILITY element body isUNAVAILABLE(lands, not silently dropped; spins its own agent + server harness)tests/MTConnect.NET-Integration-Tests/Workflows/AddObservationEmptyResultUnavailableSampleStreamWorkflowTests.cs— HTTP/samplestream:UNAVAILABLE(6-case[Theory])UNAVAILABLE); proves the coerce does not silently drop the second observationAll wire surfaces fed by the agent's buffer read the same coerced payload — every transport (HTTP, MQTT relay, SHDR adapter output) inherits the fix because the coerce sits above the buffer.
Files touched
libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs— invoke the coerce inAddObservation; addIsEmptyResult+CoerceEmptyResultToUnavailablehelpers.tests/MTConnect.NET-Common-Tests/Agents/AddObservationEmptyResultCoerceTests.cs— new unit fixture.tests/MTConnect.NET-Integration-Tests/Workflows/AddObservationEmptyResultUnavailableWorkflowTests.cs— new wire-level E2E fixture against HTTP/current.tests/MTConnect.NET-Integration-Tests/Workflows/AddObservationEmptyResultUnavailableSampleStreamWorkflowTests.cs— new wire-level E2E fixture against HTTP/samplestream..github/workflows/dotnet.yml— drop matrix expressions from the two matrix jobs'name:fields so skipped-state status checks render cleanly (build-and-test/route-check-e2einstead ofbuild-and-test-${{ matrix.os }}).docs/reference/configuration.md— regenerated to surface the newdeviceValidationLevelconfig key on the docs site (IAgentConfiguration+AgentConfigurationsections). Same regenerator (docs/scripts/generate-reference.sh) that drives the docs-site drift gate.Additional scope (from cascade rebase)
Includes maintainer commit
a61a3647 Added separate DeviceValidationLevel propery and enum to handle MTConnectDevices validation:MTConnect.Agents.DeviceValidationLevel(Ignore/Warning/Remove/Strict) — controls how the agent reacts whenMTConnectDevicesshape data fails validation against the standard.AgentConfiguration.DeviceValidationLevel/IAgentConfiguration.DeviceValidationLevelproperty (defaultWarning). Serialised key:deviceValidationLevel.MTConnectAgent.NormalizeDevice— the three invalid-Component / -Composition / -DataItem branches now consultDeviceValidationLevelinstead ofInputValidationLevel, splitting device-shape validation from observation validation. EnablesInputValidationLevel = Strict(spec-conformant observations) withDeviceValidationLevel = Warning(accept non-standard device shapes).Orthogonal to the empty-Result coerce contract above; the coerce runs unconditionally in
AddObservationregardless of either level, per the Part 1MUST.