b4480bf45a
The fixture Agent flips to AllowPut = true (its comment rewritten: writes are in
scope now that the driver implements IWritable) and the suite grows three
env-gated write legs, all still Assert.Skip-clean with no Agent reachable.
Devices.xml gains a DataItem, and that is the deliberate part. The write target
has to be one the SHDR adapter NEVER feeds, or the assertion races the stream --
but it must NOT be the existing never-fed fixture_block: the first successful
write takes an item off UNAVAILABLE for the life of the container, which would
silently rob Read_maps_an_unavailable_observation_to_BadNoCommunication of its
subject. So fixture_writeback (free-text EVENT, PROGRAM_COMMENT) is a separate,
dedicated target, overridable via MTCONNECT_WRITE_TARGET for a run pointed at a
real machine tool.
The three legs:
* Round trip -- driver write accepted, then the injected value read back off
/current over plain HTTP (not through the parser under test) AND through the
driver's own ReadAsync. The value carries a TickCount64 suffix: the Agent
keeps an injected observation for the container's life, so a constant would
be satisfied by a previous run's leftover.
* Agent refusal -> BadCommunicationError. The design doc doubted this leg was
constructible ("an id in the probe but refused by the agent"); it is, and the
mechanism is BuildWriteTargets. Write targets are derived from the tags the
BINDING serves, with the device resolved from the probe model and falling
back to the configured deviceName -- so an authored tag naming an id the
Agent never declared, on a driver whose deviceName IS set, resolves to a real
URL and is POSTed. That is exactly the shape a stale tag has after a DataItem
is renamed. The second half of the test is the falsifiability control: the
SAME unknown id with NO deviceName has no URL and must be refused locally as
BadNodeIdUnknown. Two different codes is what proves the first write actually
reached the Agent rather than being short-circuited into a look-alike.
* Disabled gate -- write.enabled absent returns BadNotWritable AND /current is
unchanged from the snapshot taken just before, which is what distinguishes
"refused" from "reported refused and sent anyway".
RAN LIVE, not just compiled: the fixture was brought up locally (OrbStack,
MTCONNECT_AGENT_HOST_PORT=5555 -- macOS AirPlay squats :5000) and the whole
suite passed 15/15, 0 skipped. Evidence beyond the green: the Agent reports
<ProgramComment dataItemId="fixture_writeback">OTOPCUA-WRITE-2CBCB530</...> and
its log carries "Error processing request:
/OtFixtureCnc?otopcua_no_such_data_item_2CBCB3D9=refuse-me" -- the refusal leg
demonstrably went to the wire. Offline (no Agent) the suite is 15 skipped, 0
failed; build 0 warnings / 0 errors.
Note for anyone reading agent.cfg: BufferSize = 14 is an EXPONENT (2^14 = 16384
observations), not a 14-deep ring, and /current is answered from the Agent's
per-DataItem checkpoint anyway -- so eviction is not a hazard for a write
read-back even on this busy fixture stream.
LiveOptions/ConfigJson/LiveDriverAsync gained writeEnabled + deviceName
parameters, both defaulted off/null so every pre-existing test still runs
against the shipped read-only driver whatever the Agent's AllowPut says. The
config JSON sources write.enabled FROM the options object, so the parsed config
can never disagree with the options a test thinks it built.
127 lines
8.1 KiB
XML
127 lines
8.1 KiB
XML
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!--
|
|
MTConnect integration-test device model — the canned Devices.xml the fixture Agent
|
|
(mtconnect/agent) serves from /probe.
|
|
|
|
This model is authored to exercise every branch of MTConnectDataTypeInference.Infer,
|
|
and to cover the two things the driver's canned unit-test fixtures structurally CANNOT:
|
|
|
|
1. UPPER_SNAKE type spellings. A probe document writes DataItem@type in UPPER_SNAKE
|
|
(PART_COUNT, ROTARY_VELOCITY); a streams document names the same concept in
|
|
PascalCase (PartCount, RotaryVelocity). The inference is separator-insensitive to
|
|
bridge the two, and a real Agent is the only thing that proves it — an earlier
|
|
revision typed PART_COUNT as String and passed every unit test.
|
|
|
|
2. name != id on every named DataItem. The hand-authored unit fixtures use ids like
|
|
"dev1_pos" that double as browse names; on a real machine tool the two always
|
|
differ, and DriverAttributeInfo.FullName must be the *id* (the observation
|
|
correlation key) while the browse name is the *name*. Every DataItem below that
|
|
carries a name deliberately gives it a value unequal to its id.
|
|
|
|
Inference coverage map (category / type / representation -> expected DriverDataType):
|
|
|
|
SAMPLE, units -> Float64 fixture_x_pos, fixture_x_load, fixture_c_speed
|
|
SAMPLE, TIME_SERIES, sampleCount -> Float64[] array fixture_c_temp_series
|
|
EVENT, PART_COUNT -> Int64 fixture_partcount (the regression case)
|
|
EVENT, LINE_NUMBER -> Int64 fixture_linenumber
|
|
EVENT, controlled vocabulary -> String fixture_execution, fixture_avail, fixture_mode
|
|
EVENT, free text -> String fixture_program, fixture_block (never fed),
|
|
fixture_writeback (never fed; write target)
|
|
EVENT, DATA_SET representation -> String fixture_varset
|
|
CONDITION (any type) -> String + IsAlarm fixture_x_travel, fixture_logic
|
|
|
|
fixture_block is deliberately NEVER fed by the adapter, so the Agent reports it
|
|
UNAVAILABLE forever — the live half of the UNAVAILABLE -> BadNoCommunication mapping.
|
|
|
|
fixture_writeback is likewise never fed, and exists for a different reason: it is the
|
|
target the write legs POST to (agent.cfg has AllowPut = true). It is a SEPARATE item from
|
|
fixture_block on purpose — the first successful write would take fixture_block off
|
|
UNAVAILABLE for the life of the container and quietly rob the mapping test above of its
|
|
subject. Because no adapter ever writes fixture_writeback, the value a write leg reads back
|
|
out of /current can only have come from that write.
|
|
-->
|
|
<MTConnectDevices
|
|
xmlns="urn:mtconnect.org:MTConnectDevices:2.0"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xsi:schemaLocation="urn:mtconnect.org:MTConnectDevices:2.0 http://schemas.mtconnect.org/schemas/MTConnectDevices_2.0.xsd">
|
|
<Header creationTime="2026-07-24T00:00:00Z" sender="otopcua-fixture" instanceId="0" version="2.0" assetBufferSize="128" assetCount="0" bufferSize="16384"/>
|
|
<Devices>
|
|
<!--
|
|
The Adapters block in agent.cfg is keyed by this Device's NAME (OtFixtureCnc). Rename
|
|
one and you must rename the other, or the Agent starts with no adapter attached and
|
|
every observation stays UNAVAILABLE.
|
|
-->
|
|
<Device id="otfixture" name="OtFixtureCnc" uuid="otopcua-mtconnect-fixture">
|
|
<Description manufacturer="OtOpcUa" model="IntegrationFixture" serialNumber="fixture-001">MTConnect integration-test fixture</Description>
|
|
<DataItems>
|
|
<!-- Device-level data items, declared OUTSIDE any component: the walker must not drop these. -->
|
|
<DataItem category="EVENT" id="fixture_avail" name="Favail" type="AVAILABILITY"/>
|
|
<DataItem category="EVENT" id="fixture_asset_changed" type="ASSET_CHANGED"/>
|
|
<DataItem category="EVENT" id="fixture_asset_removed" type="ASSET_REMOVED"/>
|
|
</DataItems>
|
|
<Components>
|
|
<Axes id="fixture_axes" name="Axes">
|
|
<Components>
|
|
<Linear id="fixture_x" name="X">
|
|
<DataItems>
|
|
<!-- SAMPLE + units -> Float64. Driven as a sinusoid, so its value CHANGES between reads. -->
|
|
<DataItem category="SAMPLE" id="fixture_x_pos" name="Xact" type="POSITION" subType="ACTUAL" units="MILLIMETER" nativeUnits="MILLIMETER"/>
|
|
<DataItem category="SAMPLE" id="fixture_x_load" name="Xload" type="LOAD" units="PERCENT" nativeUnits="PERCENT"/>
|
|
<!-- CONDITION -> String + IsAlarm, whatever its type says. -->
|
|
<DataItem category="CONDITION" id="fixture_x_travel" name="Xtravel" type="POSITION"/>
|
|
</DataItems>
|
|
</Linear>
|
|
<Rotary id="fixture_c" name="C">
|
|
<DataItems>
|
|
<DataItem category="SAMPLE" id="fixture_c_speed" name="Cspeed" type="ROTARY_VELOCITY" subType="ACTUAL" units="REVOLUTION/MINUTE" nativeUnits="REVOLUTION/MINUTE"/>
|
|
<!--
|
|
TIME_SERIES -> Float64 ARRAY, with ArrayDim NULL (variable length).
|
|
|
|
There is deliberately no sampleCount attribute here, and there cannot be:
|
|
`sampleCount` is an attribute of a TIME_SERIES *observation*, not of a
|
|
DataItem declaration. A real Agent rejects it outright: it logs
|
|
"The following keys were present and not expected: sampleCount" followed by
|
|
"DataItems: Invalid element 'DataItem'" and DROPS THE ENTIRE DATA ITEM from
|
|
the device model, so the tag simply vanishes from /probe (verified live
|
|
against mtconnect/agent 2.7.0.12). MTConnectDataTypeInference therefore only
|
|
ever sees sampleCount = null from a real Agent, and stamps ArrayDim = null.
|
|
`sampleRate` IS a valid DataItem attribute and is kept.
|
|
-->
|
|
<DataItem category="SAMPLE" id="fixture_c_temp_series" name="Ctemps" type="TEMPERATURE" units="CELSIUS" nativeUnits="CELSIUS" representation="TIME_SERIES" sampleRate="100"/>
|
|
</DataItems>
|
|
</Rotary>
|
|
</Components>
|
|
</Axes>
|
|
<Controller id="fixture_controller" name="Controller">
|
|
<DataItems>
|
|
<!-- Controlled vocabulary EVENT -> String. -->
|
|
<DataItem category="EVENT" id="fixture_mode" name="Cmode" type="CONTROLLER_MODE"/>
|
|
</DataItems>
|
|
<Components>
|
|
<Path id="fixture_path" name="Path">
|
|
<DataItems>
|
|
<DataItem category="EVENT" id="fixture_execution" name="Pexec" type="EXECUTION"/>
|
|
<!-- THE regression case: UPPER_SNAKE PART_COUNT must infer Int64, not String. -->
|
|
<DataItem category="EVENT" id="fixture_partcount" name="Pcount" type="PART_COUNT"/>
|
|
<DataItem category="EVENT" id="fixture_linenumber" name="Pline" type="LINE_NUMBER"/>
|
|
<DataItem category="EVENT" id="fixture_program" name="Pprogram" type="PROGRAM"/>
|
|
<!-- Free-text EVENT the adapter NEVER feeds -> stays UNAVAILABLE. -->
|
|
<DataItem category="EVENT" id="fixture_block" name="Pblock" type="BLOCK"/>
|
|
<!--
|
|
The write-back target (MTCONNECT_WRITE_TARGET overrides the id when the suite is
|
|
pointed at a real Agent). Free-text EVENT, so any string a write leg sends is a
|
|
legal value, and never fed by the adapter, so nothing races the assertion.
|
|
-->
|
|
<DataItem category="EVENT" id="fixture_writeback" name="Pwriteback" type="PROGRAM_COMMENT"/>
|
|
<!-- DATA_SET representation demotes to String even though the Agent sends key=value entries. -->
|
|
<DataItem category="EVENT" id="fixture_varset" name="Pvars" type="VARIABLE" representation="DATA_SET"/>
|
|
<DataItem category="CONDITION" id="fixture_logic" name="Plogic" type="LOGIC_PROGRAM"/>
|
|
</DataItems>
|
|
</Path>
|
|
</Components>
|
|
</Controller>
|
|
</Components>
|
|
</Device>
|
|
</Devices>
|
|
</MTConnectDevices>
|