Compare commits
9 Commits
3ea72f29dd
...
6a22d28098
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a22d28098 | |||
| df77617051 | |||
| 6ae6287360 | |||
| 43b81b6643 | |||
| c90b1a8214 | |||
| 82a0c298ce | |||
| 6cd6876229 | |||
| c306c6f674 | |||
| 2dc55ecb8f |
@@ -115,7 +115,10 @@ The driver has two interchangeable wire transports, selected by one `DriverConfi
|
||||
- **Forward Open throttle:** at most one attempt in flight, at least 1 s between starts, bounded by the
|
||||
open budget.
|
||||
- `ConnectionSize` / `Timeout` are honoured per pooled connection, and the **first** driver/device to open
|
||||
a given device key wins.
|
||||
a given device key wins. ControlLogix, GuardLogix and (since T21) CompactLogix default to 4002, a Large
|
||||
Forward Open (0x5B), the same as libplctag. A target that refuses Large gets one regular open at
|
||||
min(size, 504) inside the same throttled attempt. An explicit `ConnectionSize` in the options or on the
|
||||
device overrides the family default.
|
||||
- **Routing:** Micro800 is connected with an empty route regardless of `UseConnectedMessaging`.
|
||||
SLC500/MicroLogix/PLC-5 over this driver are direct. `logixpccc` uses UnconnectedSend; its SLC framing is
|
||||
**unverified**.
|
||||
|
||||
@@ -116,14 +116,14 @@ Only the `_S`-suffix naming classifier (PR 12, `SecurityClassification.ViewOnly`
|
||||
forced on safety tags) runs. Actual safety-partition write rejection — what
|
||||
happens when a non-safety write lands on a real `1756-L8xS` — is not exercised.
|
||||
|
||||
### 5. CompactLogix narrow ConnectionSize cap
|
||||
### 5. CompactLogix connection size and the regular-open fallback
|
||||
|
||||
CompactLogix profile `Notes`: *"ab_server lacks the narrower limit itself."*
|
||||
|
||||
Driver-side `AbCipPlcFamilyProfile` caps `ConnectionSize` at the CompactLogix
|
||||
value per PR 10, but `ab_server` accepts whatever the client asks for — the
|
||||
cap's correctness is trusted from its unit test, never stressed against a
|
||||
simulator that rejects oversized requests.
|
||||
Since T21 the CompactLogix profile defaults to a **Large** Forward Open (4002), matching what libplctag
|
||||
opens for `plc=compactlogix`. The old 504 default was cosmetic under libplctag, which never received it,
|
||||
and cost the native transport ~3× the round trips per poll. A pre-v20 CompactLogix (1769-L3x) or a bridge that refuses
|
||||
the Large open is handled by `CipConnection`'s fallback (a regular open at min(size, 504) in the same
|
||||
throttled attempt). That fallback is pinned by unit tests against a fake target only: `ab_server` accepts
|
||||
any size, so a refusing controller is never exercised here.
|
||||
|
||||
### 6. BOOL-within-DINT read-modify-write (#181)
|
||||
|
||||
@@ -168,7 +168,7 @@ skip cleanly + the ab_server Docker fixture runs as usual.
|
||||
| `@tags` Symbol Object walk with `Program:` scope | partial | **yes** | yes |
|
||||
| Add-On Instructions | no | **yes** | yes |
|
||||
| GuardLogix safety-partition write rejection | no | **yes** (Emulate 5580) | yes |
|
||||
| CompactLogix narrow ConnectionSize enforcement | no | **yes** (5370 firmware) | yes |
|
||||
| CompactLogix Large-open refusal → regular-open fallback | no | **yes** (5370 firmware) | yes |
|
||||
| EtherNet/IP embedded-switch behaviour | no | no | yes |
|
||||
| Redundant chassis failover (1756-RM) | no | no | yes |
|
||||
| Motion control timing | no | no | yes |
|
||||
@@ -208,7 +208,7 @@ project is authored.
|
||||
| "Do ALMD alarms raise + clear?" | no | yes | **yes** | yes |
|
||||
| "Is Micro800 unconnected-only enforced wire-side?" | partial (--plc=Micro800 boots, but wire rejection untested) | partial | yes | yes (required) |
|
||||
| "Does GuardLogix reject non-safety writes on safety tags?" | no | no | yes (Emulate 5580) | yes |
|
||||
| "Does CompactLogix refuse oversized ConnectionSize?" | no | partial | yes (5370 firmware) | yes |
|
||||
| "Does CompactLogix refuse a Large Forward Open?" | no | partial | yes (5370 firmware) | yes |
|
||||
| "Does BOOL-in-DINT RMW race against concurrent writers?" | no | yes | partial | yes (stress) |
|
||||
| "Does EtherNet/IP embedded-switch behave correctly?" | no | no | no | yes (required) |
|
||||
| "Does redundant-chassis failover work?" | no | no | no | yes (required) |
|
||||
|
||||
@@ -185,6 +185,46 @@ see the same split on the press.
|
||||
The ab_server controllogix matrix is read-only and unaffected: 15 passed and 11 skipped with
|
||||
`AB_SERVER_COMPOSE_PROFILE=controllogix`.
|
||||
|
||||
### Latency harness proven on ab_server (T21 Step 1)
|
||||
|
||||
`Differential/PollLatencyComparisonTests.cs` (+ `PollLatencyRun.cs`) ran 2026-10-07 against ab_server
|
||||
(`controllogix` image, host port 44828 because an unrelated outbound socket held 44818 on the docker host),
|
||||
under the same `--filter "FullyQualifiedName~Differential"` invocation as the differential. The mechanics work:
|
||||
the timed phase reuses the differential's browse, the anchors, the open budget and the barriers; **1** native Forward Open
|
||||
in the latency phase; 30 passes per transport, 0 tag failures; native packs 100 concurrent reads into **6** requests per pass.
|
||||
ab_server has 7 atomic scalars, so the 100-tag set was **repeated cyclically** (reported in the output).
|
||||
|
||||
| Family | libplctag p50 / p95 ms | native p50 / p95 ms | native requests/pass | Criterion |
|
||||
|---|---|---|---|---|
|
||||
| CompactLogix (504, the press default) | 7.7 / 10.9 | 22.1 / 25.3 | 6 | **FAIL** |
|
||||
| ControlLogix (4002) | 7.3 / 9.1 | 7.7 / 9.2 | 2 | pass |
|
||||
|
||||
The CompactLogix result was a real finding, not a harness fault. Native opened a 504-byte connection there, while libplctag
|
||||
opened a Large one (see the T18 capture above), so native needed 3× the round trips per pass.
|
||||
|
||||
**Fixed:** the CompactLogix profile now defaults to 4002, so native opens Large like libplctag. A refusal still falls back to
|
||||
a regular 504 open. Re-run 2026-10-07 23:30–23:32Z against the `compactlogix` compose profile on :44818 (3 distinct atomic
|
||||
tags, repeated to 100), with a capture in the container's network namespace:
|
||||
|
||||
| Run | libplctag p50 / p95 ms | native p50 / p95 ms | native req/pass | Criterion |
|
||||
|---|---|---|---|---|
|
||||
| 1 | — | — | — | aborted in the latency libplctag phase (see below) |
|
||||
| 2 | 7.6 / 9.6 | 7.5 / 10.8 | 2 | FAIL (limit 10.6) |
|
||||
| 3 | 7.8 / 9.3 | 8.1 / 9.4 | 2 | PASS (limit 10.2) |
|
||||
| 4 | 7.7 / 10.0 | 7.8 / 11.7 | 2 | FAIL (limit 11.1) |
|
||||
| 5 | — | — | — | aborted in the latency libplctag phase |
|
||||
|
||||
The structural gap is gone: p50s are equal, and both transports now send 2 requests per pass. At ~10 ms the p95 margin is about
|
||||
1 ms, which is within this network path's jitter, so the ab_server verdict varies from run to run.
|
||||
|
||||
Capture: every session opened with **0x5B (Large Forward Open)** and was accepted (0xDB, status 0). That covers 15 sessions:
|
||||
four per complete invocation and three in the aborted run 5. Each session had one Forward Close (0x4E to Connection Manager 6/1).
|
||||
There were **zero** 0x4D/0x53/0x4B. Both aborts (runs 1 and 5) were the simulator: libplctag's ~2 KB packed request was split
|
||||
across TCP segments (1448 + 606), and ab_server answered with FIN. The harness aborted on the first connection-level failure,
|
||||
as designed. A real controller reassembles TCP; ab_server does not.
|
||||
|
||||
The T21 press result is recorded under `## Latency (T21)` and is not filled in here.
|
||||
|
||||
## AdminUI /run (T19)
|
||||
|
||||
Run 2026-10-07 on the local docker-dev rig (OrbStack), against the ab_server `controllogix` fixture
|
||||
@@ -260,8 +300,160 @@ falsified by reverting that wiring: 2/2 tests fail.
|
||||
|
||||
## Real press (T21)
|
||||
|
||||
_(pending)_
|
||||
Target: **Z-35162**, production CompactLogix 5069-L320ERS2, `10.209.70.160:44818`, path `1,0`, family CompactLogix
|
||||
(native connection size 4002, Large Forward Open). Read-only. User approved the press and each time window; VPN up;
|
||||
no other client on this Mac connected to the press. Driven from the Mac over VPN (≈44 ms RTT).
|
||||
|
||||
### Pass 1 — 2026-10-07 23:35–23:36 UTC (HEAD `c306c6f6`; 25 tags, 20 templates, pace 50 ms, latency 100×30)
|
||||
|
||||
12/14 passed. **Safety held**: packet capture (`tcpdump` on `utun4`) shows 4 sequential sessions, each one Large
|
||||
Forward Open (0x5B) and one Forward Close (0x4E, Connection Manager 6/1, acked); services seen only 0x03, 0x0A, 0x4C,
|
||||
0x52, 0x55, 0x5B, 0x4E; **zero writes** (no 0x4D/0x53/0x4B). Browse parity and all 20 UDT template shapes identical;
|
||||
native Forward Opens = 1.
|
||||
|
||||
Failures, and what they led to:
|
||||
- **Read parity, 2 tags** — live analog values moved between the sequential libplctag and native phases
|
||||
(`WW_M4ActualTemp` 384.887 → 385.138). Harness defect: it could not tell drift from divergence. Fixed by a volatility
|
||||
probe (`82a0c298`, tightened element-wise in `43b81b66`): a value that moves between two libplctag reads is
|
||||
compared by status, length, type, shape and **every element that held still**.
|
||||
- **Latency** — native p95 142.8 ms vs libplctag 99.1 ms ×1.10 = 109.1 ms (FAIL), medians native 93.7 / libplctag
|
||||
95.2 ms; three native outliers in 30 passes. Criterion unchanged; pass count raised to 50 and per-pass GC/RTT
|
||||
diagnostics added.
|
||||
- **Test host crashed at exit** — `A callback was made on a garbage collected delegate … callback_func_ex`. Root cause
|
||||
is in libplctag.NET 1.5.2: a failed tag create leaves its native callback registered and `Dispose()` doesn't release
|
||||
it. **Production-reachable** on the default transport (a nonexistent tag name crashes the server node at
|
||||
stop/restart). Fixed in the adapters (`6cd68762`, hardened `6ae62873`); Gitea #578.
|
||||
- Found before pass 1, on ab_server: native CompactLogix opened a 504-byte connection (the family default libplctag
|
||||
never honoured) and needed ~3× the round-trips. Native default changed to 4002 with the existing Large→regular
|
||||
fallback (`c306c6f6`).
|
||||
|
||||
### Pass 2 — 2026-10-08 00:21–00:24 UTC (HEAD `df776170`, fresh build; 200 tags, 20 templates, pace 50 ms, latency 100×50)
|
||||
|
||||
**17/17 passed.**
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| Browse parity | libplctag 3162 tags, native 3162 — identical sets |
|
||||
| UDT templates | 20 compared (of 74 found, capped) — identical shapes |
|
||||
| Read parity | 200 tags: **0 unexpected**, 0 expected (TypeMismatch) divergences; 6 live tags (below) |
|
||||
| Native Forward Opens | 1 per native phase |
|
||||
| Native Forward Closes sent/acked | 1/1 per native phase; 0 connections left |
|
||||
| Sessions | libplctag and native never connected at the same time |
|
||||
| Writes | none — read-only source guard passes; `grep -c WriteAsync` = 0 over every Differential file |
|
||||
| libplctag failed-create orphans | 15 destroyed by the #578 fix, 0 rooted — no crash at exit |
|
||||
|
||||
⚠️ **Pass 2 has no independent packet capture**: the operator's `tcpdump` had stopped at 23:45 UTC, before pass 2 began,
|
||||
and this was not noticed until afterwards. The read-only evidence for pass 2 is the source guard and the harness's
|
||||
own Forward Open/Close counters; pass 1's capture is the wire-level proof for the same code paths.
|
||||
|
||||
Live tags (value moved between the two libplctag reads; every element that held still matched native):
|
||||
|
||||
```
|
||||
- live: WW_M4ActualTemp (lib 4 byte(s) 0CCDC243 = 389.60193; lib probe 4 byte(s) 7084C243 = 389.03467; native 4 byte(s) 4C47C243 = 388.557)
|
||||
- live: Program:EM01_PowerUnit.__fout9FB1561FA00D73F5 (lib 20 byte(s) 0050C348218986402189A640DF76D9C021898640 = [400000,4.2042394,5.2042394,-6.7957606,4.2042394]; lib probe 20 byte(s) 0050C348E74B8640E74BA64019B4D9C0E74B8640 = [400000,4.1967654,5.1967654,-6.8032346,4.1967654]; native 20 byte(s) 0050C348BB328640BB32A64045CDD9C0BB328640 = [400000,4.1936927,5.1936927,-6.8063073,4.1936927])
|
||||
- live: Program:EM02_Clamp.__fout2F67CD35C2302EE0 (lib 16 byte(s) BFB2BF379A9919BF00D68349CDCCCCBE = [2.285221E-05,-0.6,1080000,-0.4]; lib probe 16 byte(s) 5119313A9A9919BF00D68349CDCCCCBE = [0.00067557866,-0.6,1080000,-0.4]; native 16 byte(s) 1562C2B99A9919BF00D68349CDCCCCBE = [-0.0003707564,-0.6,1080000,-0.4])
|
||||
- live: Program:ES03_EM01_Zone1.__fout25DE96F3D0E2B7BB (lib 16 byte(s) 00E14CC0CDDA75BF000000C08F2D2BC0 = [-3.201233,-0.9603699,-2,-2.6746557]; lib probe 16 byte(s) 009464C09A2589BF000000C0A84932C0 = [-3.5715332,-1.07146,-2,-2.7857456]; native 16 byte(s) 00A03AC034F35FBF000000C0A8B325C0 = [-2.9160156,-0.87480474,-2,-2.5890903])
|
||||
- live: Program:ES03_EM01_Zone1.__fout92848E133B44AF24 (lib 32 byte(s) 40EDA94160F0D043000090C10022A14980256C41C656D143006A4F400000A8C1 = [21.240845,417.87793,-18,1320000,14.759155,418.67792,3.2408447,-21]; lib probe 32 byte(s) 6017AD412089D043000090C10022A14940D1654186EFD04300BB68400000A8C1 = [21.636414,417.0713,-18,1320000,14.363586,417.87128,3.6364136,-21]; native 32 byte(s) C047A841BE97CF43000090C10022A14980706F4124FECF43003E42400000A8C1 = [21.035034,415.1855,-18,1320000,14.964966,415.98547,3.0350342,-21])
|
||||
- live: Program:ES03_EM01_Zone1.__fout6DC7745628658A9D (lib 8 byte(s) 00824E40CD34A53F = [3.2266846,1.2906739]; lib probe 8 byte(s) 008A6440CDD4B63F = [3.5709229,1.4283692]; native 8 byte(s) 000D3940670A943F = [2.8914185,1.1565675])
|
||||
```
|
||||
|
||||
Test summary:
|
||||
|
||||
```
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.DifferentialSafetyTests.Template_parity_fails_when_vacuous_or_undecodable [26 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.DifferentialSafetyTests.A_failing_endpoint_aborts_the_native_phase_after_exactly_one_connect [2 s]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.DifferentialSafetyTests.The_default_selection_keeps_arrays_and_udt_members_at_any_cap [17 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.DifferentialSafetyTests.Volume_knobs_fail_fast_above_the_ceiling_instead_of_clamping [4 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.DifferentialSafetyTests.A_failing_endpoint_aborts_the_libplctag_phase_before_any_tag_is_read [6 s]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.PollLatencyComparisonTests.Tag_pick_takes_atomic_scalars_only_and_repeats_a_short_set_visibly [15 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.PollLatencyComparisonTests.Native_poll_p95_is_within_ten_percent_of_libplctag [3 m 2 s]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.The_native_side_closes_its_one_connection_with_exactly_one_forward_close [< 1 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.No_failed_libplctag_tag_is_left_with_a_live_native_callback [< 1 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.A_live_value_is_compared_structurally_and_a_stable_one_by_value [3 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.The_harness_is_read_only_by_construction [36 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.Browse_listings_decode_to_identical_tag_sets_through_both_enumerators [6 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.The_comparison_flags_every_divergence_and_excuses_only_a_native_type_mismatch [< 1 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.Every_template_decodes_to_an_identical_shape_through_both_readers [< 1 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.The_native_side_makes_exactly_one_forward_open_over_the_whole_run [< 1 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.Every_selected_tag_reads_identically_through_both_transports [3 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.Libplctag_and_native_are_never_connected_at_the_same_time [< 1 ms]
|
||||
Total tests: 17
|
||||
Passed: 17
|
||||
```
|
||||
|
||||
Full raw `dotnet test` output: [pass 1](2026-10-07-native-ab-transport-t21/t21-pass1.txt), [pass 2](2026-10-07-native-ab-transport-t21/t21-pass2.txt).
|
||||
|
||||
## Latency (T21)
|
||||
|
||||
_(pending)_
|
||||
Pass 2 (100 atomic tags × 50 passes per transport, 200 ms gap, all reads in a pass issued concurrently):
|
||||
|
||||
| Transport | Passes | p50 ms | p95 ms | min ms | max ms | CIP requests per pass (median) | Tag failures |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| libplctag | 50 | 107.0 | 139.5 | 57.9 | 164.8 | n/a | 0 |
|
||||
| native | 50 | 100.3 | 105.8 | 94.8 | 107.5 | 2 | 0 |
|
||||
|
||||
**Criterion: native p95 105.8 ms ≤ libplctag p95 139.5 ms × 1.10 = 153.4 ms → PASS.** Native was faster at the median
|
||||
and much steadier (max 107.5 ms vs 164.8 ms), packing 95 reads into 2 CIP requests per pass. Five pad/config tags that
|
||||
libplctag refused at setup were removed from both sets (`Local:*:I.Pad1`, `Local:5:C.CfgSize`, `Local:5:C.CfgIDNum`).
|
||||
|
||||
Pass 1 (30 passes) failed this criterion on three native outliers (p95 142.8 vs limit 109.1; medians native 93.7 /
|
||||
libplctag 95.2) — see above.
|
||||
|
||||
Per-pass detail (pass 2):
|
||||
|
||||
```
|
||||
| Pass | libplctag ms | libplctag GC 0/1/2 | native ms | native GC 0/1/2 | native requests | native RTT min/mean/max ms |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | 104.8 | 0/0/0 | 100.3 | 0/0/0 | 2 | 44.9/49.6/54.4 |
|
||||
| 2 | 57.9 | 0/0/0 | 100.5 | 0/0/0 | 2 | 47.0/50.1/53.3 |
|
||||
| 3 | 139.5 | 0/0/0 | 105.8 | 0/0/0 | 2 | 45.3/52.9/60.4 |
|
||||
| 4 | 104.5 | 0/0/0 | 100.6 | 1/0/0 | 2 | 47.3/50.2/53.2 |
|
||||
| 5 | 108.0 | 0/0/0 | 105.3 | 0/0/0 | 2 | 44.5/52.6/60.6 |
|
||||
| 6 | 104.4 | 0/0/0 | 105.6 | 0/0/0 | 2 | 44.2/52.7/61.2 |
|
||||
| 7 | 60.8 | 0/0/0 | 98.7 | 0/0/0 | 2 | 44.3/49.3/54.2 |
|
||||
| 8 | 59.6 | 0/0/0 | 102.6 | 0/0/0 | 2 | 47.4/51.2/55.0 |
|
||||
| 9 | 109.2 | 0/0/0 | 97.0 | 0/0/0 | 2 | 45.9/48.4/50.9 |
|
||||
| 10 | 109.7 | 0/0/0 | 99.3 | 0/0/0 | 2 | 45.9/49.6/53.3 |
|
||||
| 11 | 105.9 | 0/0/0 | 103.0 | 0/0/0 | 2 | 46.2/51.4/56.7 |
|
||||
| 12 | 107.7 | 0/0/0 | 95.1 | 0/0/0 | 2 | 43.3/47.5/51.7 |
|
||||
| 13 | 104.7 | 0/0/0 | 104.0 | 0/0/0 | 2 | 45.7/52.0/58.2 |
|
||||
| 14 | 106.8 | 0/0/0 | 96.5 | 0/0/0 | 2 | 43.6/48.2/52.7 |
|
||||
| 15 | 107.2 | 0/0/0 | 102.4 | 0/0/0 | 2 | 46.1/51.1/56.2 |
|
||||
| 16 | 105.5 | 0/0/0 | 97.4 | 0/0/0 | 2 | 44.7/48.6/52.6 |
|
||||
| 17 | 105.9 | 0/0/0 | 107.5 | 0/0/0 | 2 | 51.0/53.7/56.4 |
|
||||
| 18 | 103.2 | 0/0/0 | 96.6 | 0/0/0 | 2 | 43.0/48.2/53.5 |
|
||||
| 19 | 106.5 | 0/0/0 | 102.1 | 0/0/0 | 2 | 45.5/51.0/56.4 |
|
||||
| 20 | 106.8 | 0/0/0 | 99.6 | 0/0/0 | 2 | 44.6/49.7/54.9 |
|
||||
| 21 | 122.8 | 0/0/0 | 104.5 | 0/0/0 | 2 | 52.1/52.2/52.3 |
|
||||
| 22 | 109.8 | 0/0/0 | 104.3 | 0/0/0 | 2 | 45.8/52.1/58.5 |
|
||||
| 23 | 104.3 | 0/0/0 | 98.6 | 0/0/0 | 2 | 45.4/49.2/53.0 |
|
||||
| 24 | 109.7 | 0/0/0 | 98.8 | 0/0/0 | 2 | 45.9/49.3/52.8 |
|
||||
| 25 | 155.1 | 0/0/0 | 101.0 | 0/0/0 | 2 | 45.5/50.4/55.4 |
|
||||
| 26 | 164.8 | 0/0/0 | 102.9 | 0/0/0 | 2 | 43.9/51.4/58.8 |
|
||||
| 27 | 120.4 | 0/0/0 | 101.4 | 0/0/0 | 2 | 48.2/50.7/53.1 |
|
||||
| 28 | 108.6 | 0/0/0 | 101.7 | 0/0/0 | 2 | 46.9/50.8/54.7 |
|
||||
| 29 | 106.0 | 0/0/0 | 98.5 | 0/0/0 | 2 | 45.5/49.2/52.9 |
|
||||
| 30 | 102.9 | 0/0/0 | 94.9 | 0/0/0 | 2 | 43.6/47.4/51.2 |
|
||||
| 31 | 111.3 | 0/0/0 | 99.9 | 0/0/0 | 2 | 45.6/49.9/54.2 |
|
||||
| 32 | 103.3 | 0/0/0 | 96.4 | 0/0/0 | 2 | 43.6/48.1/52.6 |
|
||||
| 33 | 105.8 | 0/0/0 | 104.6 | 0/0/0 | 2 | 46.4/52.2/58.0 |
|
||||
| 34 | 109.3 | 0/0/0 | 95.8 | 0/0/0 | 2 | 43.2/47.8/52.4 |
|
||||
| 35 | 110.2 | 0/0/0 | 102.1 | 0/0/0 | 2 | 46.3/51.0/55.7 |
|
||||
| 36 | 108.0 | 0/0/0 | 101.7 | 0/0/0 | 2 | 47.5/50.8/54.0 |
|
||||
| 37 | 107.6 | 0/0/0 | 96.7 | 0/0/0 | 2 | 45.0/48.3/51.5 |
|
||||
| 38 | 108.9 | 0/0/0 | 96.5 | 0/0/0 | 2 | 44.7/48.2/51.6 |
|
||||
| 39 | 105.2 | 0/0/0 | 98.2 | 0/0/0 | 2 | 45.6/49.0/52.5 |
|
||||
| 40 | 106.7 | 0/0/0 | 99.4 | 0/0/0 | 2 | 44.4/49.6/54.9 |
|
||||
| 41 | 107.0 | 0/0/0 | 101.8 | 0/0/0 | 2 | 46.9/50.8/54.8 |
|
||||
| 42 | 108.6 | 0/0/0 | 106.7 | 0/0/0 | 2 | 49.2/53.3/57.4 |
|
||||
| 43 | 107.0 | 0/0/0 | 102.6 | 0/0/0 | 2 | 49.9/51.2/52.6 |
|
||||
| 44 | 107.7 | 0/0/0 | 100.3 | 0/0/0 | 2 | 47.4/50.1/52.7 |
|
||||
| 45 | 109.5 | 0/0/0 | 102.6 | 0/0/0 | 2 | 45.8/51.2/56.7 |
|
||||
| 46 | 107.6 | 0/0/0 | 104.0 | 0/0/0 | 2 | 48.4/52.0/55.5 |
|
||||
| 47 | 115.6 | 0/0/0 | 99.0 | 0/0/0 | 2 | 45.9/49.4/52.9 |
|
||||
| 48 | 106.0 | 0/0/0 | 97.8 | 0/0/0 | 2 | 44.9/48.8/52.8 |
|
||||
| 49 | 104.5 | 0/0/0 | 98.4 | 0/0/0 | 2 | 46.1/49.1/52.2 |
|
||||
| 50 | 112.5 | 0/0/0 | 94.8 | 1/1/1 | 2 | 43.0/47.3/51.7 |
|
||||
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
|
||||
Test run for /Users/dohertj2/Desktop/OtOpcUa/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/bin/Debug/net10.0/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.dll (.NETCoreApp,Version=v10.0)
|
||||
VSTest version 18.0.1 (arm64)
|
||||
|
||||
Starting test execution, please wait...
|
||||
A total of 1 test files matched the specified pattern.
|
||||
/Users/dohertj2/Desktop/OtOpcUa/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/bin/Debug/net10.0/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.dll
|
||||
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v3.0.2+dd36e86129 (64-bit .NET 10.0.5)
|
||||
[xUnit.net 00:00:00.33] Discovering: ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests
|
||||
[xUnit.net 00:00:00.46] Discovered: ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests
|
||||
[xUnit.net 00:00:00.57] Starting: ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.DifferentialSafetyTests.Template_parity_fails_when_vacuous_or_undecodable [11 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.DifferentialSafetyTests.A_failing_endpoint_aborts_the_native_phase_after_exactly_one_connect [2 s]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.DifferentialSafetyTests.The_default_selection_keeps_arrays_and_udt_members_at_any_cap [7 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.DifferentialSafetyTests.Volume_knobs_fail_fast_above_the_ceiling_instead_of_clamping [2 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.DifferentialSafetyTests.A_failing_endpoint_aborts_the_libplctag_phase_before_any_tag_is_read [6 s]
|
||||
Standard Output Messages:
|
||||
libplctag connects before the abort: 1
|
||||
|
||||
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.PollLatencyComparisonTests.Tag_pick_takes_atomic_scalars_only_and_repeats_a_short_set_visibly [13 ms]
|
||||
[xUnit.net 00:01:15.30] ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.PollLatencyComparisonTests.Native_poll_p95_is_within_ten_percent_of_libplctag [FAIL]
|
||||
[xUnit.net 00:01:15.30] Shouldly.ShouldAssertException : natP95
|
||||
[xUnit.net 00:01:15.30] should be less than or equal to
|
||||
[xUnit.net 00:01:15.30] 109.06115000000001d
|
||||
[xUnit.net 00:01:15.30] but was
|
||||
[xUnit.net 00:01:15.30] 142.765d
|
||||
[xUnit.net 00:01:15.30]
|
||||
[xUnit.net 00:01:15.30] Additional Info:
|
||||
[xUnit.net 00:01:15.30] native p95 must be ≤ libplctag p95 × 1.10
|
||||
[xUnit.net 00:01:15.30] Stack Trace:
|
||||
[xUnit.net 00:01:15.30] /Users/dohertj2/Desktop/OtOpcUa/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Differential/PollLatencyComparisonTests.cs(74,0): at ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.PollLatencyComparisonTests.Native_poll_p95_is_within_ten_percent_of_libplctag()
|
||||
[xUnit.net 00:01:15.30] at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
|
||||
[xUnit.net 00:01:15.30] at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
|
||||
[xUnit.net 00:01:15.30] at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task, ConfigureAwaitOptions options)
|
||||
[xUnit.net 00:01:15.30] --- End of stack trace from previous location ---
|
||||
[xUnit.net 00:01:15.30] at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
|
||||
[xUnit.net 00:01:15.30] at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
|
||||
[xUnit.net 00:01:15.30] at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task, ConfigureAwaitOptions options)
|
||||
[xUnit.net 00:01:15.30] Output:
|
||||
[xUnit.net 00:01:15.30] ## Latency — 10.209.70.160:44818 path '1,0' family CompactLogix (compactlogix, connection size 4002), max tags 25, max templates 20, pace 50 ms
|
||||
[xUnit.net 00:01:15.30] Tags per pass: 95 (100 distinct); passes per transport: 30; gap between passes: 200 ms.
|
||||
[xUnit.net 00:01:15.30] Types: SInt 6, DInt 8, Bool 17, Real 7, Int 57
|
||||
[xUnit.net 00:01:15.30] Dropped (libplctag refused at setup, removed from both sets): Local:1:I.Pad1, Local:2:I.Pad1, Local:3:I.Pad1, Local:5:C.CfgSize, Local:5:C.CfgIDNum
|
||||
[xUnit.net 00:01:15.30] Native requests spent on setup (one read per tag): 95; native Forward Opens: 1.
|
||||
[xUnit.net 00:01:15.30]
|
||||
[xUnit.net 00:01:15.30] | Transport | Passes | p50 ms | p95 ms | min ms | max ms | CIP requests per pass (median) | Tag failures |
|
||||
[xUnit.net 00:01:15.30] |---|---|---|---|---|---|---|---|
|
||||
[xUnit.net 00:01:15.30] | libplctag | 30 | 95.2 | 99.1 | 90.8 | 179.2 | n/a | 0 |
|
||||
[xUnit.net 00:01:15.30] | native | 30 | 93.7 | 142.8 | 88.8 | 146.1 | 2 | 0 |
|
||||
[xUnit.net 00:01:15.30]
|
||||
[xUnit.net 00:01:15.30] Criterion: native p95 142.8 ms ≤ libplctag p95 99.1 ms × 1.10 = 109.1 ms → **FAIL**
|
||||
[xUnit.net 00:01:15.30]
|
||||
[xUnit.net 00:01:15.30] | Pass | libplctag ms | native ms | native requests |
|
||||
[xUnit.net 00:01:15.30] |---|---|---|---|
|
||||
[xUnit.net 00:01:15.30] | 1 | 95.0 | 95.4 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 2 | 94.4 | 92.4 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 3 | 95.8 | 96.8 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 4 | 95.0 | 94.7 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 5 | 98.8 | 93.7 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 6 | 92.4 | 101.0 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 7 | 94.1 | 94.1 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 8 | 97.0 | 96.7 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 9 | 93.4 | 91.2 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 10 | 97.7 | 91.0 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 11 | 94.6 | 91.1 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 12 | 96.5 | 92.0 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 13 | 94.1 | 97.6 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 14 | 96.1 | 94.0 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 15 | 90.8 | 93.3 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 16 | 95.6 | 93.7 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 17 | 93.9 | 100.6 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 18 | 92.4 | 121.6 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 19 | 97.5 | 92.7 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 20 | 95.2 | 93.8 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 21 | 96.8 | 90.4 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 22 | 98.6 | 99.1 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 23 | 92.1 | 146.1 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 24 | 99.1 | 142.8 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 25 | 96.6 | 91.7 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 26 | 96.9 | 91.4 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 27 | 179.2 | 92.0 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 28 | 98.5 | 91.7 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 29 | 95.0 | 88.8 | 2 |
|
||||
[xUnit.net 00:01:15.30] | 30 | 95.0 | 93.4 | 2 |
|
||||
Failed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.PollLatencyComparisonTests.Native_poll_p95_is_within_ten_percent_of_libplctag [1 m 6 s]
|
||||
Error Message:
|
||||
Shouldly.ShouldAssertException : natP95
|
||||
should be less than or equal to
|
||||
109.06115000000001d
|
||||
but was
|
||||
142.765d
|
||||
|
||||
Additional Info:
|
||||
native p95 must be ≤ libplctag p95 × 1.10
|
||||
Stack Trace:
|
||||
at ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.PollLatencyComparisonTests.Native_poll_p95_is_within_ten_percent_of_libplctag() in /Users/dohertj2/Desktop/OtOpcUa/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Differential/PollLatencyComparisonTests.cs:line 74
|
||||
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
|
||||
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
|
||||
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task, ConfigureAwaitOptions options)
|
||||
--- End of stack trace from previous location ---
|
||||
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
|
||||
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
|
||||
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task, ConfigureAwaitOptions options)
|
||||
Standard Output Messages:
|
||||
## Latency — 10.209.70.160:44818 path '1,0' family CompactLogix (compactlogix, connection size 4002), max tags 25, max templates 20, pace 50 ms
|
||||
Tags per pass: 95 (100 distinct); passes per transport: 30; gap between passes: 200 ms.
|
||||
Types: SInt 6, DInt 8, Bool 17, Real 7, Int 57
|
||||
Dropped (libplctag refused at setup, removed from both sets): Local:1:I.Pad1, Local:2:I.Pad1, Local:3:I.Pad1, Local:5:C.CfgSize, Local:5:C.CfgIDNum
|
||||
Native requests spent on setup (one read per tag): 95; native Forward Opens: 1.
|
||||
|
||||
| Transport | Passes | p50 ms | p95 ms | min ms | max ms | CIP requests per pass (median) | Tag failures |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| libplctag | 30 | 95.2 | 99.1 | 90.8 | 179.2 | n/a | 0 |
|
||||
| native | 30 | 93.7 | 142.8 | 88.8 | 146.1 | 2 | 0 |
|
||||
|
||||
Criterion: native p95 142.8 ms ≤ libplctag p95 99.1 ms × 1.10 = 109.1 ms → **FAIL**
|
||||
|
||||
| Pass | libplctag ms | native ms | native requests |
|
||||
|---|---|---|---|
|
||||
| 1 | 95.0 | 95.4 | 2 |
|
||||
| 2 | 94.4 | 92.4 | 2 |
|
||||
| 3 | 95.8 | 96.8 | 2 |
|
||||
| 4 | 95.0 | 94.7 | 2 |
|
||||
| 5 | 98.8 | 93.7 | 2 |
|
||||
| 6 | 92.4 | 101.0 | 2 |
|
||||
| 7 | 94.1 | 94.1 | 2 |
|
||||
| 8 | 97.0 | 96.7 | 2 |
|
||||
| 9 | 93.4 | 91.2 | 2 |
|
||||
| 10 | 97.7 | 91.0 | 2 |
|
||||
| 11 | 94.6 | 91.1 | 2 |
|
||||
| 12 | 96.5 | 92.0 | 2 |
|
||||
| 13 | 94.1 | 97.6 | 2 |
|
||||
| 14 | 96.1 | 94.0 | 2 |
|
||||
| 15 | 90.8 | 93.3 | 2 |
|
||||
| 16 | 95.6 | 93.7 | 2 |
|
||||
| 17 | 93.9 | 100.6 | 2 |
|
||||
| 18 | 92.4 | 121.6 | 2 |
|
||||
| 19 | 97.5 | 92.7 | 2 |
|
||||
| 20 | 95.2 | 93.8 | 2 |
|
||||
| 21 | 96.8 | 90.4 | 2 |
|
||||
| 22 | 98.6 | 99.1 | 2 |
|
||||
| 23 | 92.1 | 146.1 | 2 |
|
||||
| 24 | 99.1 | 142.8 | 2 |
|
||||
| 25 | 96.6 | 91.7 | 2 |
|
||||
| 26 | 96.9 | 91.4 | 2 |
|
||||
| 27 | 179.2 | 92.0 | 2 |
|
||||
| 28 | 98.5 | 91.7 | 2 |
|
||||
| 29 | 95.0 | 88.8 | 2 |
|
||||
| 30 | 95.0 | 93.4 | 2 |
|
||||
|
||||
|
||||
[xUnit.net 00:01:15.35] ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.Every_selected_tag_reads_identically_through_both_transports [FAIL]
|
||||
[xUnit.net 00:01:15.35] Shouldly.ShouldAssertException : unexpected
|
||||
[xUnit.net 00:01:15.35] should be empty but had
|
||||
[xUnit.net 00:01:15.35] 2
|
||||
[xUnit.net 00:01:15.35] items and was
|
||||
[xUnit.net 00:01:15.35] ["WW_M4ActualTemp: **NO: raw** (lib 4 byte(s) 8471C043 = 384.88684; native 4 byte(s) A491C043 = 385.13782)", "Program:EM01_PowerUnit.__fout9FB1561FA00D73F5: **NO: raw** (lib 20 byte(s) 0050C3481CE387401CE3A740E41CD8C01CE38740 = [400000,4.2464733,5.2464733,-6.7535267,4.2464733]; native 20 byte(s) 0050C34811DD874011DDA740EF22D8C011DD8740 = [400000,4.2457356,5.2457356,-6.7542644,4.2457356])"]
|
||||
[xUnit.net 00:01:15.35] Stack Trace:
|
||||
[xUnit.net 00:01:15.35] /Users/dohertj2/Desktop/OtOpcUa/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Differential/TransportDifferentialTests.cs(123,0): at ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.Every_selected_tag_reads_identically_through_both_transports()
|
||||
[xUnit.net 00:01:15.35] at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
|
||||
[xUnit.net 00:01:15.35] at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
|
||||
[xUnit.net 00:01:15.35] at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task, ConfigureAwaitOptions options)
|
||||
[xUnit.net 00:01:15.35] --- End of stack trace from previous location ---
|
||||
[xUnit.net 00:01:15.35] at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
|
||||
[xUnit.net 00:01:15.35] at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
|
||||
[xUnit.net 00:01:15.35] at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task, ConfigureAwaitOptions options)
|
||||
[xUnit.net 00:01:15.35] Output:
|
||||
[xUnit.net 00:01:15.35] ## Reads — 25 tag(s), 10.209.70.160:44818 path '1,0' family CompactLogix (compactlogix, connection size 4002), max tags 25, max templates 20, pace 50 ms
|
||||
[xUnit.net 00:01:15.35] Selection by origin: array 9, udt member 8, scalar 8
|
||||
[xUnit.net 00:01:15.35] | Tag | Type | libplctag status | native status | Equal? |
|
||||
[xUnit.net 00:01:15.35] |---|---|---|---|---|
|
||||
[xUnit.net 00:01:15.35] | `Program:Recipes._monitorID` | DInt[255] | Ok | Ok | yes |
|
||||
[xUnit.net 00:01:15.35] | `Local:1:I.InfoBits` | SInt | Ok | Ok | yes |
|
||||
[xUnit.net 00:01:15.35] | `__CONTAINER` | DInt | Ok | Ok | yes |
|
||||
[xUnit.net 00:01:15.35] | `Program:EM01_PowerUnit._VacuumCalibration_mA` | Real[10] | Ok | Ok | yes |
|
||||
[xUnit.net 00:01:15.35] | `Local:1:I.RunMode` | Bool | Ok | Ok | yes |
|
||||
[xUnit.net 00:01:15.35] | `WW_M4ActualTemp` | Real | Ok | Ok | **NO: raw** |
|
||||
[xUnit.net 00:01:15.35] | `Program:EM01_PowerUnit._VacuumCalibration_PSIA` | Real[10] | Ok | Ok | yes |
|
||||
[xUnit.net 00:01:15.35] | `Local:1:I.ConnectionFaulted` | Bool | Ok | Ok | yes |
|
||||
[xUnit.net 00:01:15.35] | `WW_P1_Status_Msg` | DInt | Ok | Ok | yes |
|
||||
[xUnit.net 00:01:15.35] | `Program:EM01_PowerUnit.__fout9FB1561FA00D73F5` | Real[5] | Ok | Ok | **NO: raw** |
|
||||
[xUnit.net 00:01:15.35] | `Local:1:I.DiagnosticActive` | Bool | Ok | Ok | yes |
|
||||
[xUnit.net 00:01:15.35] | `DI_CycleStart_PB` | Bool | Ok | Ok | yes |
|
||||
[xUnit.net 00:01:15.35] | `Program:EM01_PowerUnit.__foutA0A97D0B7D734500` | Real[2] | Ok | Ok | yes |
|
||||
[xUnit.net 00:01:15.35] | `Local:1:I.DiagnosticSequenceCount` | SInt | Ok | Ok | yes |
|
||||
[xUnit.net 00:01:15.35] | `test` | DInt | Ok | Ok | yes |
|
||||
[xUnit.net 00:01:15.35] | `Program:EM01_PowerUnit.__fout2A759FA1596561EC` | Real[2] | Ok | Ok | yes |
|
||||
[xUnit.net 00:01:15.35] | `Local:1:I.Pad1` | Int | NotFound | NotFound | yes |
|
||||
[xUnit.net 00:01:15.35] | `UN01_EventsPTR` | DInt | Ok | Ok | yes |
|
||||
[xUnit.net 00:01:15.35] | `Program:EM01_PowerUnit._SEQ_EP` | DInt[32] | Ok | Ok | yes |
|
||||
[xUnit.net 00:01:15.35] | `Local:2:I.InfoBits` | SInt | Ok | Ok | yes |
|
||||
[xUnit.net 00:01:15.35] | `Sts_Vacuum_Rerun` | Bool | Ok | Ok | yes |
|
||||
[xUnit.net 00:01:15.35] | `Program:EM01_PowerUnit.__fout02198332D12EF5E3` | Real[3] | Ok | Ok | yes |
|
||||
[xUnit.net 00:01:15.35] | `Local:2:I.RunMode` | Bool | Ok | Ok | yes |
|
||||
[xUnit.net 00:01:15.35] | `Ons_VacuumRerun` | Bool | Ok | Ok | yes |
|
||||
[xUnit.net 00:01:15.35] | `Program:EM02_Clamp._SEQ_EP` | DInt[32] | Ok | Ok | yes |
|
||||
[xUnit.net 00:01:15.35]
|
||||
[xUnit.net 00:01:15.35] Unexpected divergences: 2; expected divergences: 0.
|
||||
[xUnit.net 00:01:15.35] - UNEXPECTED: WW_M4ActualTemp: **NO: raw** (lib 4 byte(s) 8471C043 = 384.88684; native 4 byte(s) A491C043 = 385.13782)
|
||||
[xUnit.net 00:01:15.35] - UNEXPECTED: Program:EM01_PowerUnit.__fout9FB1561FA00D73F5: **NO: raw** (lib 20 byte(s) 0050C3481CE387401CE3A740E41CD8C01CE38740 = [400000,4.2464733,5.2464733,-6.7535267,4.2464733]; native 20 byte(s) 0050C34811DD874011DDA740EF22D8C011DD8740 = [400000,4.2457356,5.2457356,-6.7542644,4.2457356])
|
||||
[xUnit.net 00:01:15.35] Finished: ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests
|
||||
Process terminated.
|
||||
A callback was made on a garbage collected delegate of type 'libplctag.NativeImport!libplctag.NativeImport.plctag+callback_func_ex::Invoke'.
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.The_harness_is_read_only_by_construction [31 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.Browse_listings_decode_to_identical_tag_sets_through_both_enumerators [6 ms]
|
||||
Standard Output Messages:
|
||||
## Browse — 10.209.70.160:44818 path '1,0' family CompactLogix (compactlogix, connection size 4002), max tags 25, max templates 20, pace 50 ms
|
||||
libplctag: 3162 tag(s); native: 3162 tag(s)
|
||||
|
||||
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.The_comparison_flags_every_divergence_and_excuses_only_a_native_type_mismatch [< 1 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.Every_template_decodes_to_an_identical_shape_through_both_readers [< 1 ms]
|
||||
Standard Output Messages:
|
||||
## Templates — 20 id(s); truncated by AB_DIFF_MAX_TEMPLATES: libplctag 74, native 74
|
||||
| Template | libplctag | native | Equal? |
|
||||
|---|---|---|---|
|
||||
| 283 | Recipe (5 members, 1680 B) | Recipe (5 members, 1680 B) | yes |
|
||||
| 295 | UDT_PartStatus (5 members, 4 B) | UDT_PartStatus (5 members, 4 B) | yes |
|
||||
| 344 | Recipe_Clamp (7 members, 420 B) | Recipe_Clamp (7 members, 420 B) | yes |
|
||||
| 491 | ForceSegmentEnableCheck (3 members, 4 B) | ForceSegmentEnableCheck (3 members, 4 B) | yes |
|
||||
| 527 | Recipe_PowerUnit (6 members, 24 B) | Recipe_PowerUnit (6 members, 24 B) | yes |
|
||||
| 622 | UDT_TemperatureProfile_Segment (11 members, 40 B) | UDT_TemperatureProfile_Segment (11 members, 40 B) | yes |
|
||||
| 676 | AB:5000_DO8_Diag:I:0 (14 members, 36 B) | AB:5000_DO8_Diag:I:0 (14 members, 36 B) | yes |
|
||||
| 700 | AB:5000_AI4CJ:I:0 (12 members, 68 B) | AB:5000_AI4CJ:I:0 (12 members, 68 B) | yes |
|
||||
| 706 | raM_UDT_ItfAD_5000Digital_Sts (11 members, 320 B) | raM_UDT_ItfAD_5000Digital_Sts (11 members, 320 B) | yes |
|
||||
| 715 | TwoHand_Control (8 members, 16 B) | TwoHand_Control (8 members, 16 B) | yes |
|
||||
| 752 | STR0016 (2 members, 20 B) | STR0016 (2 members, 20 B) | yes |
|
||||
| 842 | STR0032 (2 members, 36 B) | STR0032 (2 members, 36 B) | yes |
|
||||
| 862 | raM_UDT_ItfAD_5000Analog_Set (3 members, 4 B) | raM_UDT_ItfAD_5000Analog_Set (3 members, 4 B) | yes |
|
||||
| 1129 | raM_Tec_LimitWithModulo (38 members, 120 B) | raM_Tec_LimitWithModulo (38 members, 120 B) | yes |
|
||||
| 1144 | PartStatus (8 members, 12 B) | PartStatus (8 members, 12 B) | yes |
|
||||
| 1166 | AB:5000_DI16:C:0 (18 members, 72 B) | AB:5000_DI16:C:0 (18 members, 72 B) | yes |
|
||||
| 1192 | raM_UDT_ItfAD_5000Digital_Cmd (5 members, 4 B) | raM_UDT_ItfAD_5000Digital_Cmd (5 members, 4 B) | yes |
|
||||
| 1206 | raM_Dvc_5000AI4_VCRT (77 members, 180 B) | raM_Dvc_5000AI4_VCRT (77 members, 180 B) | yes |
|
||||
| 1289 | TimeString (12 members, 376 B) | TimeString (12 members, 376 B) | yes |
|
||||
| 1313 | raM_Dvc_5000AO4 (73 members, 164 B) | raM_Dvc_5000AO4 (73 members, 164 B) | yes |
|
||||
|
||||
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.The_native_side_makes_exactly_one_forward_open_over_the_whole_run [< 1 ms]
|
||||
Standard Output Messages:
|
||||
Native Forward Opens over the run: 1.
|
||||
|
||||
|
||||
Failed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.Every_selected_tag_reads_identically_through_both_transports [2 ms]
|
||||
Error Message:
|
||||
Shouldly.ShouldAssertException : unexpected
|
||||
should be empty but had
|
||||
2
|
||||
items and was
|
||||
["WW_M4ActualTemp: **NO: raw** (lib 4 byte(s) 8471C043 = 384.88684; native 4 byte(s) A491C043 = 385.13782)", "Program:EM01_PowerUnit.__fout9FB1561FA00D73F5: **NO: raw** (lib 20 byte(s) 0050C3481CE387401CE3A740E41CD8C01CE38740 = [400000,4.2464733,5.2464733,-6.7535267,4.2464733]; native 20 byte(s) 0050C34811DD874011DDA740EF22D8C011DD8740 = [400000,4.2457356,5.2457356,-6.7542644,4.2457356])"]
|
||||
Stack Trace:
|
||||
at ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.Every_selected_tag_reads_identically_through_both_transports() in /Users/dohertj2/Desktop/OtOpcUa/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Differential/TransportDifferentialTests.cs:line 123
|
||||
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
|
||||
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
|
||||
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task, ConfigureAwaitOptions options)
|
||||
--- End of stack trace from previous location ---
|
||||
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
|
||||
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
|
||||
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task, ConfigureAwaitOptions options)
|
||||
Standard Output Messages:
|
||||
## Reads — 25 tag(s), 10.209.70.160:44818 path '1,0' family CompactLogix (compactlogix, connection size 4002), max tags 25, max templates 20, pace 50 ms
|
||||
Selection by origin: array 9, udt member 8, scalar 8
|
||||
| Tag | Type | libplctag status | native status | Equal? |
|
||||
|---|---|---|---|---|
|
||||
| `Program:Recipes._monitorID` | DInt[255] | Ok | Ok | yes |
|
||||
| `Local:1:I.InfoBits` | SInt | Ok | Ok | yes |
|
||||
| `__CONTAINER` | DInt | Ok | Ok | yes |
|
||||
| `Program:EM01_PowerUnit._VacuumCalibration_mA` | Real[10] | Ok | Ok | yes |
|
||||
| `Local:1:I.RunMode` | Bool | Ok | Ok | yes |
|
||||
| `WW_M4ActualTemp` | Real | Ok | Ok | **NO: raw** |
|
||||
| `Program:EM01_PowerUnit._VacuumCalibration_PSIA` | Real[10] | Ok | Ok | yes |
|
||||
| `Local:1:I.ConnectionFaulted` | Bool | Ok | Ok | yes |
|
||||
| `WW_P1_Status_Msg` | DInt | Ok | Ok | yes |
|
||||
| `Program:EM01_PowerUnit.__fout9FB1561FA00D73F5` | Real[5] | Ok | Ok | **NO: raw** |
|
||||
| `Local:1:I.DiagnosticActive` | Bool | Ok | Ok | yes |
|
||||
| `DI_CycleStart_PB` | Bool | Ok | Ok | yes |
|
||||
| `Program:EM01_PowerUnit.__foutA0A97D0B7D734500` | Real[2] | Ok | Ok | yes |
|
||||
| `Local:1:I.DiagnosticSequenceCount` | SInt | Ok | Ok | yes |
|
||||
| `test` | DInt | Ok | Ok | yes |
|
||||
| `Program:EM01_PowerUnit.__fout2A759FA1596561EC` | Real[2] | Ok | Ok | yes |
|
||||
| `Local:1:I.Pad1` | Int | NotFound | NotFound | yes |
|
||||
| `UN01_EventsPTR` | DInt | Ok | Ok | yes |
|
||||
| `Program:EM01_PowerUnit._SEQ_EP` | DInt[32] | Ok | Ok | yes |
|
||||
| `Local:2:I.InfoBits` | SInt | Ok | Ok | yes |
|
||||
| `Sts_Vacuum_Rerun` | Bool | Ok | Ok | yes |
|
||||
| `Program:EM01_PowerUnit.__fout02198332D12EF5E3` | Real[3] | Ok | Ok | yes |
|
||||
| `Local:2:I.RunMode` | Bool | Ok | Ok | yes |
|
||||
| `Ons_VacuumRerun` | Bool | Ok | Ok | yes |
|
||||
| `Program:EM02_Clamp._SEQ_EP` | DInt[32] | Ok | Ok | yes |
|
||||
|
||||
Unexpected divergences: 2; expected divergences: 0.
|
||||
- UNEXPECTED: WW_M4ActualTemp: **NO: raw** (lib 4 byte(s) 8471C043 = 384.88684; native 4 byte(s) A491C043 = 385.13782)
|
||||
- UNEXPECTED: Program:EM01_PowerUnit.__fout9FB1561FA00D73F5: **NO: raw** (lib 20 byte(s) 0050C3481CE387401CE3A740E41CD8C01CE38740 = [400000,4.2464733,5.2464733,-6.7535267,4.2464733]; native 20 byte(s) 0050C34811DD874011DDA740EF22D8C011DD8740 = [400000,4.2457356,5.2457356,-6.7542644,4.2457356])
|
||||
|
||||
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.Libplctag_and_native_are_never_connected_at_the_same_time [< 1 ms]
|
||||
Standard Output Messages:
|
||||
TCP connections to the target: baseline 0, before native 0, libplctag drain wait 5284 ms.
|
||||
|
||||
|
||||
|
||||
Test Run Failed.
|
||||
Total tests: 14
|
||||
Passed: 12
|
||||
Failed: 2
|
||||
Total time: 1.2601 Minutes
|
||||
@@ -0,0 +1,369 @@
|
||||
|
||||
Test run for /Users/dohertj2/Desktop/OtOpcUa/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/bin/Debug/net10.0/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.dll (.NETCoreApp,Version=v10.0)
|
||||
VSTest version 18.0.1 (arm64)
|
||||
|
||||
Starting test execution, please wait...
|
||||
A total of 1 test files matched the specified pattern.
|
||||
/Users/dohertj2/Desktop/OtOpcUa/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/bin/Debug/net10.0/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.dll
|
||||
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v3.0.2+dd36e86129 (64-bit .NET 10.0.5)
|
||||
[xUnit.net 00:00:00.46] Discovering: ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests
|
||||
[xUnit.net 00:00:00.70] Discovered: ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests
|
||||
[xUnit.net 00:00:00.93] Starting: ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.DifferentialSafetyTests.Template_parity_fails_when_vacuous_or_undecodable [26 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.DifferentialSafetyTests.A_failing_endpoint_aborts_the_native_phase_after_exactly_one_connect [2 s]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.DifferentialSafetyTests.The_default_selection_keeps_arrays_and_udt_members_at_any_cap [17 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.DifferentialSafetyTests.Volume_knobs_fail_fast_above_the_ceiling_instead_of_clamping [4 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.DifferentialSafetyTests.A_failing_endpoint_aborts_the_libplctag_phase_before_any_tag_is_read [6 s]
|
||||
Standard Output Messages:
|
||||
libplctag connects before the abort: 1
|
||||
|
||||
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.PollLatencyComparisonTests.Tag_pick_takes_atomic_scalars_only_and_repeats_a_short_set_visibly [15 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.PollLatencyComparisonTests.Native_poll_p95_is_within_ten_percent_of_libplctag [3 m 2 s]
|
||||
Standard Output Messages:
|
||||
## Latency — 10.209.70.160:44818 path '1,0' family CompactLogix (compactlogix, connection size 4002), max tags 200, max templates 20, pace 50 ms
|
||||
Tags per pass: 95 (100 distinct); passes per transport: 50; gap between passes: 200 ms.
|
||||
Types: SInt 6, DInt 8, Bool 17, Real 7, Int 57
|
||||
Dropped (libplctag refused at setup, removed from both sets): Local:1:I.Pad1, Local:2:I.Pad1, Local:3:I.Pad1, Local:5:C.CfgSize, Local:5:C.CfgIDNum
|
||||
Native requests spent on setup (one read per tag): 95; native Forward Opens: 1; native Forward Closes sent/acked: 1/1; connections after the native phase: 0.
|
||||
libplctag orphans destroyed so far: 15; rooted (unreleasable): 0.
|
||||
|
||||
| Transport | Passes | p50 ms | p95 ms | min ms | max ms | CIP requests per pass (median) | Tag failures |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| libplctag | 50 | 107.0 | 139.5 | 57.9 | 164.8 | n/a | 0 |
|
||||
| native | 50 | 100.3 | 105.8 | 94.8 | 107.5 | 2 | 0 |
|
||||
|
||||
Criterion: native p95 105.8 ms ≤ libplctag p95 139.5 ms × 1.10 = 153.4 ms → PASS
|
||||
|
||||
| Pass | libplctag ms | libplctag GC 0/1/2 | native ms | native GC 0/1/2 | native requests | native RTT min/mean/max ms |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | 104.8 | 0/0/0 | 100.3 | 0/0/0 | 2 | 44.9/49.6/54.4 |
|
||||
| 2 | 57.9 | 0/0/0 | 100.5 | 0/0/0 | 2 | 47.0/50.1/53.3 |
|
||||
| 3 | 139.5 | 0/0/0 | 105.8 | 0/0/0 | 2 | 45.3/52.9/60.4 |
|
||||
| 4 | 104.5 | 0/0/0 | 100.6 | 1/0/0 | 2 | 47.3/50.2/53.2 |
|
||||
| 5 | 108.0 | 0/0/0 | 105.3 | 0/0/0 | 2 | 44.5/52.6/60.6 |
|
||||
| 6 | 104.4 | 0/0/0 | 105.6 | 0/0/0 | 2 | 44.2/52.7/61.2 |
|
||||
| 7 | 60.8 | 0/0/0 | 98.7 | 0/0/0 | 2 | 44.3/49.3/54.2 |
|
||||
| 8 | 59.6 | 0/0/0 | 102.6 | 0/0/0 | 2 | 47.4/51.2/55.0 |
|
||||
| 9 | 109.2 | 0/0/0 | 97.0 | 0/0/0 | 2 | 45.9/48.4/50.9 |
|
||||
| 10 | 109.7 | 0/0/0 | 99.3 | 0/0/0 | 2 | 45.9/49.6/53.3 |
|
||||
| 11 | 105.9 | 0/0/0 | 103.0 | 0/0/0 | 2 | 46.2/51.4/56.7 |
|
||||
| 12 | 107.7 | 0/0/0 | 95.1 | 0/0/0 | 2 | 43.3/47.5/51.7 |
|
||||
| 13 | 104.7 | 0/0/0 | 104.0 | 0/0/0 | 2 | 45.7/52.0/58.2 |
|
||||
| 14 | 106.8 | 0/0/0 | 96.5 | 0/0/0 | 2 | 43.6/48.2/52.7 |
|
||||
| 15 | 107.2 | 0/0/0 | 102.4 | 0/0/0 | 2 | 46.1/51.1/56.2 |
|
||||
| 16 | 105.5 | 0/0/0 | 97.4 | 0/0/0 | 2 | 44.7/48.6/52.6 |
|
||||
| 17 | 105.9 | 0/0/0 | 107.5 | 0/0/0 | 2 | 51.0/53.7/56.4 |
|
||||
| 18 | 103.2 | 0/0/0 | 96.6 | 0/0/0 | 2 | 43.0/48.2/53.5 |
|
||||
| 19 | 106.5 | 0/0/0 | 102.1 | 0/0/0 | 2 | 45.5/51.0/56.4 |
|
||||
| 20 | 106.8 | 0/0/0 | 99.6 | 0/0/0 | 2 | 44.6/49.7/54.9 |
|
||||
| 21 | 122.8 | 0/0/0 | 104.5 | 0/0/0 | 2 | 52.1/52.2/52.3 |
|
||||
| 22 | 109.8 | 0/0/0 | 104.3 | 0/0/0 | 2 | 45.8/52.1/58.5 |
|
||||
| 23 | 104.3 | 0/0/0 | 98.6 | 0/0/0 | 2 | 45.4/49.2/53.0 |
|
||||
| 24 | 109.7 | 0/0/0 | 98.8 | 0/0/0 | 2 | 45.9/49.3/52.8 |
|
||||
| 25 | 155.1 | 0/0/0 | 101.0 | 0/0/0 | 2 | 45.5/50.4/55.4 |
|
||||
| 26 | 164.8 | 0/0/0 | 102.9 | 0/0/0 | 2 | 43.9/51.4/58.8 |
|
||||
| 27 | 120.4 | 0/0/0 | 101.4 | 0/0/0 | 2 | 48.2/50.7/53.1 |
|
||||
| 28 | 108.6 | 0/0/0 | 101.7 | 0/0/0 | 2 | 46.9/50.8/54.7 |
|
||||
| 29 | 106.0 | 0/0/0 | 98.5 | 0/0/0 | 2 | 45.5/49.2/52.9 |
|
||||
| 30 | 102.9 | 0/0/0 | 94.9 | 0/0/0 | 2 | 43.6/47.4/51.2 |
|
||||
| 31 | 111.3 | 0/0/0 | 99.9 | 0/0/0 | 2 | 45.6/49.9/54.2 |
|
||||
| 32 | 103.3 | 0/0/0 | 96.4 | 0/0/0 | 2 | 43.6/48.1/52.6 |
|
||||
| 33 | 105.8 | 0/0/0 | 104.6 | 0/0/0 | 2 | 46.4/52.2/58.0 |
|
||||
| 34 | 109.3 | 0/0/0 | 95.8 | 0/0/0 | 2 | 43.2/47.8/52.4 |
|
||||
| 35 | 110.2 | 0/0/0 | 102.1 | 0/0/0 | 2 | 46.3/51.0/55.7 |
|
||||
| 36 | 108.0 | 0/0/0 | 101.7 | 0/0/0 | 2 | 47.5/50.8/54.0 |
|
||||
| 37 | 107.6 | 0/0/0 | 96.7 | 0/0/0 | 2 | 45.0/48.3/51.5 |
|
||||
| 38 | 108.9 | 0/0/0 | 96.5 | 0/0/0 | 2 | 44.7/48.2/51.6 |
|
||||
| 39 | 105.2 | 0/0/0 | 98.2 | 0/0/0 | 2 | 45.6/49.0/52.5 |
|
||||
| 40 | 106.7 | 0/0/0 | 99.4 | 0/0/0 | 2 | 44.4/49.6/54.9 |
|
||||
| 41 | 107.0 | 0/0/0 | 101.8 | 0/0/0 | 2 | 46.9/50.8/54.8 |
|
||||
| 42 | 108.6 | 0/0/0 | 106.7 | 0/0/0 | 2 | 49.2/53.3/57.4 |
|
||||
| 43 | 107.0 | 0/0/0 | 102.6 | 0/0/0 | 2 | 49.9/51.2/52.6 |
|
||||
| 44 | 107.7 | 0/0/0 | 100.3 | 0/0/0 | 2 | 47.4/50.1/52.7 |
|
||||
| 45 | 109.5 | 0/0/0 | 102.6 | 0/0/0 | 2 | 45.8/51.2/56.7 |
|
||||
| 46 | 107.6 | 0/0/0 | 104.0 | 0/0/0 | 2 | 48.4/52.0/55.5 |
|
||||
| 47 | 115.6 | 0/0/0 | 99.0 | 0/0/0 | 2 | 45.9/49.4/52.9 |
|
||||
| 48 | 106.0 | 0/0/0 | 97.8 | 0/0/0 | 2 | 44.9/48.8/52.8 |
|
||||
| 49 | 104.5 | 0/0/0 | 98.4 | 0/0/0 | 2 | 46.1/49.1/52.2 |
|
||||
| 50 | 112.5 | 0/0/0 | 94.8 | 1/1/1 | 2 | 43.0/47.3/51.7 |
|
||||
|
||||
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.The_native_side_closes_its_one_connection_with_exactly_one_forward_close [< 1 ms]
|
||||
Standard Output Messages:
|
||||
Native Forward Closes sent/acked: 1/1; connections after the native phase: 0.
|
||||
|
||||
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.No_failed_libplctag_tag_is_left_with_a_live_native_callback [< 1 ms]
|
||||
Standard Output Messages:
|
||||
libplctag orphans destroyed so far: 15; rooted (unreleasable): 0.
|
||||
|
||||
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.A_live_value_is_compared_structurally_and_a_stable_one_by_value [3 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.The_harness_is_read_only_by_construction [36 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.Browse_listings_decode_to_identical_tag_sets_through_both_enumerators [6 ms]
|
||||
Standard Output Messages:
|
||||
## Browse — 10.209.70.160:44818 path '1,0' family CompactLogix (compactlogix, connection size 4002), max tags 200, max templates 20, pace 50 ms
|
||||
libplctag: 3162 tag(s); native: 3162 tag(s)
|
||||
|
||||
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.The_comparison_flags_every_divergence_and_excuses_only_a_native_type_mismatch [< 1 ms]
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.Every_template_decodes_to_an_identical_shape_through_both_readers [< 1 ms]
|
||||
Standard Output Messages:
|
||||
## Templates — 20 id(s); truncated by AB_DIFF_MAX_TEMPLATES: libplctag 74, native 74
|
||||
| Template | libplctag | native | Equal? |
|
||||
|---|---|---|---|
|
||||
| 283 | Recipe (5 members, 1680 B) | Recipe (5 members, 1680 B) | yes |
|
||||
| 295 | UDT_PartStatus (5 members, 4 B) | UDT_PartStatus (5 members, 4 B) | yes |
|
||||
| 344 | Recipe_Clamp (7 members, 420 B) | Recipe_Clamp (7 members, 420 B) | yes |
|
||||
| 491 | ForceSegmentEnableCheck (3 members, 4 B) | ForceSegmentEnableCheck (3 members, 4 B) | yes |
|
||||
| 527 | Recipe_PowerUnit (6 members, 24 B) | Recipe_PowerUnit (6 members, 24 B) | yes |
|
||||
| 622 | UDT_TemperatureProfile_Segment (11 members, 40 B) | UDT_TemperatureProfile_Segment (11 members, 40 B) | yes |
|
||||
| 676 | AB:5000_DO8_Diag:I:0 (14 members, 36 B) | AB:5000_DO8_Diag:I:0 (14 members, 36 B) | yes |
|
||||
| 700 | AB:5000_AI4CJ:I:0 (12 members, 68 B) | AB:5000_AI4CJ:I:0 (12 members, 68 B) | yes |
|
||||
| 706 | raM_UDT_ItfAD_5000Digital_Sts (11 members, 320 B) | raM_UDT_ItfAD_5000Digital_Sts (11 members, 320 B) | yes |
|
||||
| 715 | TwoHand_Control (8 members, 16 B) | TwoHand_Control (8 members, 16 B) | yes |
|
||||
| 752 | STR0016 (2 members, 20 B) | STR0016 (2 members, 20 B) | yes |
|
||||
| 842 | STR0032 (2 members, 36 B) | STR0032 (2 members, 36 B) | yes |
|
||||
| 862 | raM_UDT_ItfAD_5000Analog_Set (3 members, 4 B) | raM_UDT_ItfAD_5000Analog_Set (3 members, 4 B) | yes |
|
||||
| 1129 | raM_Tec_LimitWithModulo (38 members, 120 B) | raM_Tec_LimitWithModulo (38 members, 120 B) | yes |
|
||||
| 1144 | PartStatus (8 members, 12 B) | PartStatus (8 members, 12 B) | yes |
|
||||
| 1166 | AB:5000_DI16:C:0 (18 members, 72 B) | AB:5000_DI16:C:0 (18 members, 72 B) | yes |
|
||||
| 1192 | raM_UDT_ItfAD_5000Digital_Cmd (5 members, 4 B) | raM_UDT_ItfAD_5000Digital_Cmd (5 members, 4 B) | yes |
|
||||
| 1206 | raM_Dvc_5000AI4_VCRT (77 members, 180 B) | raM_Dvc_5000AI4_VCRT (77 members, 180 B) | yes |
|
||||
| 1289 | TimeString (12 members, 376 B) | TimeString (12 members, 376 B) | yes |
|
||||
| 1313 | raM_Dvc_5000AO4 (73 members, 164 B) | raM_Dvc_5000AO4 (73 members, 164 B) | yes |
|
||||
|
||||
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.The_native_side_makes_exactly_one_forward_open_over_the_whole_run [< 1 ms]
|
||||
Standard Output Messages:
|
||||
Native Forward Opens over the run: 1.
|
||||
|
||||
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.Every_selected_tag_reads_identically_through_both_transports [3 ms]
|
||||
Standard Output Messages:
|
||||
## Reads — 200 tag(s), 10.209.70.160:44818 path '1,0' family CompactLogix (compactlogix, connection size 4002), max tags 200, max templates 20, pace 50 ms
|
||||
Selection by origin: array 20, udt member 20, scalar 160
|
||||
| Tag | Type | libplctag status | native status | libplctag probe | Equal? |
|
||||
|---|---|---|---|---|---|
|
||||
| `Program:Recipes._monitorID` | DInt[255] | Ok | Ok | stable | yes |
|
||||
| `Local:1:I.InfoBits` | SInt | Ok | Ok | stable | yes |
|
||||
| `__CONTAINER` | DInt | Ok | Ok | stable | yes |
|
||||
| `Program:EM01_PowerUnit._VacuumCalibration_mA` | Real[10] | Ok | Ok | stable | yes |
|
||||
| `Local:1:I.RunMode` | Bool | Ok | Ok | stable | yes |
|
||||
| `WW_M4ActualTemp` | Real | Ok | Ok | **live** | live |
|
||||
| `Program:EM01_PowerUnit._VacuumCalibration_PSIA` | Real[10] | Ok | Ok | stable | yes |
|
||||
| `Local:1:I.ConnectionFaulted` | Bool | Ok | Ok | stable | yes |
|
||||
| `WW_P1_Status_Msg` | DInt | Ok | Ok | stable | yes |
|
||||
| `Program:EM01_PowerUnit.__fout9FB1561FA00D73F5` | Real[5] | Ok | Ok | **live** | live |
|
||||
| `Local:1:I.DiagnosticActive` | Bool | Ok | Ok | stable | yes |
|
||||
| `DI_CycleStart_PB` | Bool | Ok | Ok | stable | yes |
|
||||
| `Program:EM01_PowerUnit.__foutA0A97D0B7D734500` | Real[2] | Ok | Ok | stable | yes |
|
||||
| `Local:1:I.DiagnosticSequenceCount` | SInt | Ok | Ok | stable | yes |
|
||||
| `test` | DInt | Ok | Ok | stable | yes |
|
||||
| `Program:EM01_PowerUnit.__fout2A759FA1596561EC` | Real[2] | Ok | Ok | stable | yes |
|
||||
| `Local:1:I.Pad1` | Int | NotFound | NotFound | NotFound | yes |
|
||||
| `UN01_EventsPTR` | DInt | Ok | Ok | stable | yes |
|
||||
| `Program:EM01_PowerUnit._SEQ_EP` | DInt[32] | Ok | Ok | stable | yes |
|
||||
| `Local:2:I.InfoBits` | SInt | Ok | Ok | stable | yes |
|
||||
| `Sts_Vacuum_Rerun` | Bool | Ok | Ok | stable | yes |
|
||||
| `Program:EM01_PowerUnit.__fout02198332D12EF5E3` | Real[3] | Ok | Ok | stable | yes |
|
||||
| `Local:2:I.RunMode` | Bool | Ok | Ok | stable | yes |
|
||||
| `Ons_VacuumRerun` | Bool | Ok | Ok | stable | yes |
|
||||
| `Program:EM02_Clamp._SEQ_EP` | DInt[32] | Ok | Ok | stable | yes |
|
||||
| `Local:2:I.ConnectionFaulted` | Bool | Ok | Ok | stable | yes |
|
||||
| `test2` | DInt | Ok | Ok | stable | yes |
|
||||
| `Program:EM02_Clamp.__fout79B2713C88B35A28` | Real[3] | Ok | Ok | stable | yes |
|
||||
| `Local:2:I.DiagnosticActive` | Bool | Ok | Ok | stable | yes |
|
||||
| `_Val_PressurePVx` | Real | Ok | Ok | stable | yes |
|
||||
| `Program:EM02_Clamp.__fout2F67CD35C2302EE0` | Real[4] | Ok | Ok | **live** | live |
|
||||
| `Local:2:I.DiagnosticSequenceCount` | SInt | Ok | Ok | stable | yes |
|
||||
| `C_LimitType_Outside` | DInt | Ok | Ok | stable | yes |
|
||||
| `Program:ES03_EM01_Zone1.__fout25DE96F3D0E2B7BB` | Real[4] | Ok | Ok | **live** | live |
|
||||
| `Local:2:I.Pad1` | Int | NotFound | NotFound | NotFound | yes |
|
||||
| `Par_TargetTemperature_Tolerance` | Real | Ok | Ok | stable | yes |
|
||||
| `Program:ES03_EM01_Zone1.__fout92848E133B44AF24` | Real[8] | Ok | Ok | **live** | live |
|
||||
| `Local:3:I.InfoBits` | SInt | Ok | Ok | stable | yes |
|
||||
| `Cfg_RapidHeatup_Tolerancex` | DInt | Ok | Ok | stable | yes |
|
||||
| `Program:ES03_EM01_Zone1.__fout6DC7745628658A9D` | Real[2] | Ok | Ok | **live** | live |
|
||||
| `Local:3:I.RunMode` | Bool | Ok | Ok | stable | yes |
|
||||
| `Cfg_RapidHeatup_Tolerance` | Real | Ok | Ok | stable | yes |
|
||||
| `Program:ES03_EM01_Zone1.__fout7C6F52924950A71D` | Real[2] | Ok | Ok | stable | yes |
|
||||
| `Local:3:I.ConnectionFaulted` | Bool | Ok | Ok | stable | yes |
|
||||
| `test_stop` | Bool | Ok | Ok | stable | yes |
|
||||
| `Program:ES03_EM01_Zone1.__foutCBB7A04F2668DCD0` | Real[2] | Ok | Ok | stable | yes |
|
||||
| `Local:3:I.DiagnosticActive` | Bool | Ok | Ok | stable | yes |
|
||||
| `Sts_ScreenLock` | Bool | Ok | Ok | stable | yes |
|
||||
| `Program:ES03_TemperatureControl._DisableStates` | DInt[32] | Ok | Ok | stable | yes |
|
||||
| `Local:3:I.DiagnosticSequenceCount` | SInt | Ok | Ok | stable | yes |
|
||||
| `xSts_ClearResult` | Bool | Ok | Ok | stable | yes |
|
||||
| `Program:ES03_TemperatureControl._ModeNames` | String[32] | Ok | Ok | stable | yes |
|
||||
| `Local:3:I.Pad1` | Int | NotFound | NotFound | NotFound | yes |
|
||||
| `WS2_CLOSE_Delay_Setpoint` | Int | Ok | Ok | stable | yes |
|
||||
| `Program:ES03_TemperatureControl._ModeTransitions` | DInt[32] | Ok | Ok | stable | yes |
|
||||
| `Local:5:C.CfgSize` | DInt | NotFound | NotFound | NotFound | yes |
|
||||
| `WS2_Force_AlarmDelay_Setpoint` | Int | Ok | Ok | stable | yes |
|
||||
| `Program:ES03_TemperatureControl._SEQ_UP` | DInt[32] | Ok | Ok | stable | yes |
|
||||
| `Local:5:C.CfgIDNum` | DInt | NotFound | NotFound | NotFound | yes |
|
||||
| `WS2_Force_Red_Setpoint` | Real | Ok | Ok | stable | yes |
|
||||
| `C_LimitType_Inside` | DInt | Ok | Ok | stable | yes |
|
||||
| `xxCfg_WaterValve_MinTime` | Real | Ok | Ok | stable | yes |
|
||||
| `WS2_Force_StartDelay_Setpoint` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_Force_Yellow` | Real | Ok | Ok | stable | yes |
|
||||
| `WS2_F_E10_Mold3TempTermSetpt` | Int | Ok | Ok | stable | yes |
|
||||
| `WW_Green` | Bool | Ok | Ok | stable | yes |
|
||||
| `WW_Down` | Bool | Ok | Ok | stable | yes |
|
||||
| `WS2_F_E1_Mold3TempTermSetpt` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_F_E2_Mold3TempTermSetpt` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_F_E3_Mold3TempTermSetpt` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_F_E4_Mold3TempTermSetpt` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_F_E5_Mold3TempTermSetpt` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_F_E6_Mold3TempTermSetpt` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_F_E7_Mold3TempTermSetpt` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_F_E8_Mold3TempTermSetpt` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_F_E9_Mold3TempTermSetpt` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_HighMold_Red_Setpoint` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_LowMold_Red_Setpoint` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_Mold_AlarmDelay_Setpoint` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_Mold_StartDelay_Setpoint` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PH_E10_Mold3TempTermSetpt` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PH_E1_Mold3TempTermSetpt` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PH_E2_Mold3TempTermSetpt` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PH_E3_Mold3TempTermSetpt` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PH_E4_Mold3TempTermSetpt` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PH_E5_Mold3TempTermSetpt` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PH_E6_Mold3TempTermSetpt` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PH_E7_Mold3TempTermSetpt` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PH_E8_Mold3TempTermSetpt` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PH_E9_Mold3TempTermSetpt` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_POSITION_SP` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLDPLATEN_Z1Z3_SEG1` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLDPLATEN_Z1Z3_SEG10` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLDPLATEN_Z1Z3_SEG2` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLDPLATEN_Z1Z3_SEG3` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLDPLATEN_Z1Z3_SEG4` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLDPLATEN_Z1Z3_SEG5` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLDPLATEN_Z1Z3_SEG6` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLDPLATEN_Z1Z3_SEG7` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLDPLATEN_Z1Z3_SEG8` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLDPLATEN_Z1Z3_SEG9` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLDPLATEN_Z2Z4_SEG1` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLDPLATEN_Z2Z4_SEG10` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLDPLATEN_Z2Z4_SEG2` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLDPLATEN_Z2Z4_SEG3` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLDPLATEN_Z2Z4_SEG4` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLDPLATEN_Z2Z4_SEG5` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLDPLATEN_Z2Z4_SEG6` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLDPLATEN_Z2Z4_SEG7` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLDPLATEN_Z2Z4_SEG8` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLDPLATEN_Z2Z4_SEG9` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLD_MOLD_TEMP_SEG1` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLD_MOLD_TEMP_SEG10` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLD_MOLD_TEMP_SEG2` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLD_MOLD_TEMP_SEG3` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLD_MOLD_TEMP_SEG4` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLD_MOLD_TEMP_SEG5` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLD_MOLD_TEMP_SEG6` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLD_MOLD_TEMP_SEG7` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLD_MOLD_TEMP_SEG8` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLD_MOLD_TEMP_SEG9` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLD_Time_Setpoint_SEG1` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLD_Time_Setpoint_SEG10` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLD_Time_Setpoint_SEG2` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLD_Time_Setpoint_SEG3` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLD_Time_Setpoint_SEG4` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLD_Time_Setpoint_SEG5` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLD_Time_Setpoint_SEG6` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLD_Time_Setpoint_SEG7` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLD_Time_Setpoint_SEG8` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_HOLD_Time_Setpoint_SEG9` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_RAMP_SEG1` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_RAMP_SEG10` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_RAMP_SEG2` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_RAMP_SEG3` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_RAMP_SEG4` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_RAMP_SEG5` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_RAMP_SEG6` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_RAMP_SEG7` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_RAMP_SEG8` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_RAMP_SEG9` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_SP_SEG1` | Real | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_SP_SEG10` | Real | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_SP_SEG2` | Real | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_SP_SEG3` | Real | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_SP_SEG4` | Real | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_SP_SEG5` | Real | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_SP_SEG6` | Real | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_SP_SEG7` | Real | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_SP_SEG8` | Real | Ok | Ok | stable | yes |
|
||||
| `WS2_PSI_SP_SEG9` | Real | Ok | Ok | stable | yes |
|
||||
| `WS2_SPEED_SP` | Real | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMPPLTEN_Z1Z3_HOLD_SEG1` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMPPLTEN_Z1Z3_HOLD_SEG10` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMPPLTEN_Z1Z3_HOLD_SEG2` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMPPLTEN_Z1Z3_HOLD_SEG3` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMPPLTEN_Z1Z3_HOLD_SEG4` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMPPLTEN_Z1Z3_HOLD_SEG5` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMPPLTEN_Z1Z3_HOLD_SEG6` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMPPLTEN_Z1Z3_HOLD_SEG7` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMPPLTEN_Z1Z3_HOLD_SEG8` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMPPLTEN_Z1Z3_HOLD_SEG9` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMPPLTEN_Z2Z4_HOLD_SEG1` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMPPLTEN_Z2Z4_HOLD_SEG10` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMPPLTEN_Z2Z4_HOLD_SEG2` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMPPLTEN_Z2Z4_HOLD_SEG3` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMPPLTEN_Z2Z4_HOLD_SEG4` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMPPLTEN_Z2Z4_HOLD_SEG5` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMPPLTEN_Z2Z4_HOLD_SEG6` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMPPLTEN_Z2Z4_HOLD_SEG7` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMPPLTEN_Z2Z4_HOLD_SEG8` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMPPLTEN_Z2Z4_HOLD_SEG9` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_HOLD_Time_Setpoint_SEG1` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_HOLD_Time_Setpoint_SEG10` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_HOLD_Time_Setpoint_SEG2` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_HOLD_Time_Setpoint_SEG3` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_HOLD_Time_Setpoint_SEG4` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_HOLD_Time_Setpoint_SEG5` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_HOLD_Time_Setpoint_SEG6` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_HOLD_Time_Setpoint_SEG7` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_HOLD_Time_Setpoint_SEG8` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_HOLD_Time_Setpoint_SEG9` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_MOLD_HOLD_TEMP_SEG1` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_MOLD_HOLD_TEMP_SEG10` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_MOLD_HOLD_TEMP_SEG2` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_MOLD_HOLD_TEMP_SEG3` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_MOLD_HOLD_TEMP_SEG4` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_MOLD_HOLD_TEMP_SEG5` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_MOLD_HOLD_TEMP_SEG6` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_MOLD_HOLD_TEMP_SEG7` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_MOLD_HOLD_TEMP_SEG8` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_MOLD_HOLD_TEMP_SEG9` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_RAMP_SEG1` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_RAMP_SEG10` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_RAMP_SEG2` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_RAMP_SEG3` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_RAMP_SEG4` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_RAMP_SEG5` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_RAMP_SEG6` | Int | Ok | Ok | stable | yes |
|
||||
| `WS2_TEMP_RAMP_SEG7` | Int | Ok | Ok | stable | yes |
|
||||
|
||||
Unexpected divergences: 0; expected divergences: 0; live tags (value moved between the two libplctag reads; compared by status, length, type code, shape and every element that held still): 6 of 200.
|
||||
- live: WW_M4ActualTemp (lib 4 byte(s) 0CCDC243 = 389.60193; lib probe 4 byte(s) 7084C243 = 389.03467; native 4 byte(s) 4C47C243 = 388.557)
|
||||
- live: Program:EM01_PowerUnit.__fout9FB1561FA00D73F5 (lib 20 byte(s) 0050C348218986402189A640DF76D9C021898640 = [400000,4.2042394,5.2042394,-6.7957606,4.2042394]; lib probe 20 byte(s) 0050C348E74B8640E74BA64019B4D9C0E74B8640 = [400000,4.1967654,5.1967654,-6.8032346,4.1967654]; native 20 byte(s) 0050C348BB328640BB32A64045CDD9C0BB328640 = [400000,4.1936927,5.1936927,-6.8063073,4.1936927])
|
||||
- live: Program:EM02_Clamp.__fout2F67CD35C2302EE0 (lib 16 byte(s) BFB2BF379A9919BF00D68349CDCCCCBE = [2.285221E-05,-0.6,1080000,-0.4]; lib probe 16 byte(s) 5119313A9A9919BF00D68349CDCCCCBE = [0.00067557866,-0.6,1080000,-0.4]; native 16 byte(s) 1562C2B99A9919BF00D68349CDCCCCBE = [-0.0003707564,-0.6,1080000,-0.4])
|
||||
- live: Program:ES03_EM01_Zone1.__fout25DE96F3D0E2B7BB (lib 16 byte(s) 00E14CC0CDDA75BF000000C08F2D2BC0 = [-3.201233,-0.9603699,-2,-2.6746557]; lib probe 16 byte(s) 009464C09A2589BF000000C0A84932C0 = [-3.5715332,-1.07146,-2,-2.7857456]; native 16 byte(s) 00A03AC034F35FBF000000C0A8B325C0 = [-2.9160156,-0.87480474,-2,-2.5890903])
|
||||
- live: Program:ES03_EM01_Zone1.__fout92848E133B44AF24 (lib 32 byte(s) 40EDA94160F0D043000090C10022A14980256C41C656D143006A4F400000A8C1 = [21.240845,417.87793,-18,1320000,14.759155,418.67792,3.2408447,-21]; lib probe 32 byte(s) 6017AD412089D043000090C10022A14940D1654186EFD04300BB68400000A8C1 = [21.636414,417.0713,-18,1320000,14.363586,417.87128,3.6364136,-21]; native 32 byte(s) C047A841BE97CF43000090C10022A14980706F4124FECF43003E42400000A8C1 = [21.035034,415.1855,-18,1320000,14.964966,415.98547,3.0350342,-21])
|
||||
- live: Program:ES03_EM01_Zone1.__fout6DC7745628658A9D (lib 8 byte(s) 00824E40CD34A53F = [3.2266846,1.2906739]; lib probe 8 byte(s) 008A6440CDD4B63F = [3.5709229,1.4283692]; native 8 byte(s) 000D3940670A943F = [2.8914185,1.1565675])
|
||||
|
||||
|
||||
[xUnit.net 00:03:12.61] Finished: ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests
|
||||
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.Libplctag_and_native_are_never_connected_at_the_same_time [< 1 ms]
|
||||
Standard Output Messages:
|
||||
TCP connections to the target: baseline 0, before native 0, libplctag drain wait 1 ms.
|
||||
|
||||
|
||||
|
||||
Test Run Successful.
|
||||
Total tests: 17
|
||||
Passed: 17
|
||||
Total time: 3.2143 Minutes
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Eip;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip;
|
||||
|
||||
@@ -23,9 +24,55 @@ public sealed class CipConnection : ICipChannel, IAsyncDisposable
|
||||
/// <summary>Forward Open frames sent by every connection in the process (fallback included). Gate diagnostics.</summary>
|
||||
internal static int ForwardOpenCount;
|
||||
|
||||
/// <summary>Forward Close requests put on the wire by every connection in the process. Gate diagnostics.</summary>
|
||||
internal static int ForwardClosesSent;
|
||||
|
||||
/// <summary>Forward Close requests that got a reply within their budget. Gate diagnostics.</summary>
|
||||
internal static int ForwardClosesAcked;
|
||||
|
||||
/// <summary>Channel sends by every connection in the process (keep-alives included). Gate diagnostics.</summary>
|
||||
internal static int RequestsSent;
|
||||
|
||||
// Round-trip statistics of completed exchanges, process-wide (gate diagnostics: T21 latency attribution).
|
||||
// Two Stopwatch reads and four interlocked ops per request; reset by the harness between timed passes.
|
||||
private static long _exchangeCount;
|
||||
private static long _exchangeTotalTicks;
|
||||
private static long _exchangeMinTicks = long.MaxValue;
|
||||
private static long _exchangeMaxTicks;
|
||||
|
||||
/// <summary>Round-trip statistics of the exchanges completed since the last <see cref="ResetExchangeStats"/>.</summary>
|
||||
internal readonly record struct ExchangeStats(long Count, double MinMs, double MaxMs, double MeanMs);
|
||||
|
||||
/// <summary>Clears the process-wide round-trip statistics. Gate diagnostics.</summary>
|
||||
internal static void ResetExchangeStats()
|
||||
{
|
||||
Interlocked.Exchange(ref _exchangeCount, 0);
|
||||
Interlocked.Exchange(ref _exchangeTotalTicks, 0);
|
||||
Interlocked.Exchange(ref _exchangeMinTicks, long.MaxValue);
|
||||
Interlocked.Exchange(ref _exchangeMaxTicks, 0);
|
||||
}
|
||||
|
||||
/// <summary>Snapshot of the process-wide round-trip statistics. Gate diagnostics.</summary>
|
||||
internal static ExchangeStats ReadExchangeStats()
|
||||
{
|
||||
var count = Interlocked.Read(ref _exchangeCount);
|
||||
if (count == 0) return new ExchangeStats(0, 0, 0, 0);
|
||||
static double Ms(long ticks) => ticks * 1000.0 / Stopwatch.Frequency;
|
||||
return new ExchangeStats(count, Ms(Interlocked.Read(ref _exchangeMinTicks)), Ms(Interlocked.Read(ref _exchangeMaxTicks)),
|
||||
Ms(Interlocked.Read(ref _exchangeTotalTicks)) / count);
|
||||
}
|
||||
|
||||
private static void RecordExchange(long ticks)
|
||||
{
|
||||
Interlocked.Increment(ref _exchangeCount);
|
||||
Interlocked.Add(ref _exchangeTotalTicks, ticks);
|
||||
long seen;
|
||||
while (ticks < (seen = Interlocked.Read(ref _exchangeMinTicks))
|
||||
&& Interlocked.CompareExchange(ref _exchangeMinTicks, ticks, seen) != seen) { }
|
||||
while (ticks > (seen = Interlocked.Read(ref _exchangeMaxTicks))
|
||||
&& Interlocked.CompareExchange(ref _exchangeMaxTicks, ticks, seen) != seen) { }
|
||||
}
|
||||
|
||||
/// <summary>Open attempts (TCP connect + RegisterSession + Forward Open, a fallback open included) started by
|
||||
/// every connection in the process — counted before any I/O, so a refused attempt counts too. Gate diagnostics.</summary>
|
||||
internal static int OpenAttempts;
|
||||
@@ -246,14 +293,17 @@ public sealed class CipConnection : ICipChannel, IAsyncDisposable
|
||||
Interlocked.Exchange(ref _lastSendTimestamp, _time.GetTimestamp());
|
||||
using var deadline = new CancellationTokenSource(ExchangeBudget, _time);
|
||||
using var linked = CancellationTokenSource.CreateLinkedTokenSource(ct, deadline.Token);
|
||||
var started = Stopwatch.GetTimestamp();
|
||||
try
|
||||
{
|
||||
return _o.Routing switch
|
||||
var reply = _o.Routing switch
|
||||
{
|
||||
CipRouting.Connected => await open.Client.SendUnitDataAsync(open.Conn!.OTConnectionId, cip, _o.Timeout, linked.Token).ConfigureAwait(false),
|
||||
CipRouting.UnconnectedSend => await open.Client.SendRRDataAsync(ConnectionManagerServices.UnconnectedSend(cip.Span, _route), _o.Timeout, linked.Token).ConfigureAwait(false),
|
||||
_ => await open.Client.SendRRDataAsync(cip, _o.Timeout, linked.Token).ConfigureAwait(false),
|
||||
};
|
||||
RecordExchange(Stopwatch.GetTimestamp() - started);
|
||||
return reply;
|
||||
}
|
||||
catch (OperationCanceledException) when (!ct.IsCancellationRequested && deadline.IsCancellationRequested)
|
||||
{
|
||||
@@ -346,9 +396,11 @@ public sealed class CipConnection : ICipChannel, IAsyncDisposable
|
||||
{
|
||||
var budget = TimeSpan.FromSeconds(1);
|
||||
using var cts = new CancellationTokenSource(budget, _time);
|
||||
Interlocked.Increment(ref ForwardClosesSent);
|
||||
await open.Client.SendRRDataAsync(
|
||||
ConnectionManagerServices.ForwardClose(open.ConnectionSerial, _o.VendorId, _o.OriginatorSerial, _route),
|
||||
budget, cts.Token).ConfigureAwait(false);
|
||||
Interlocked.Increment(ref ForwardClosesAcked);
|
||||
}
|
||||
catch (Exception ex) when (ex is AbTransportException or OperationCanceledException)
|
||||
{
|
||||
|
||||
@@ -129,11 +129,9 @@ public sealed class AbCipDriverOptions
|
||||
/// touching every other device on the same family.</param>
|
||||
/// <param name="ConnectionSize">Per-device override for the Forward Open
|
||||
/// ConnectionSize (Large Forward Open packet size in bytes). <c>null</c> inherits the family
|
||||
/// profile's <c>DefaultConnectionSize</c>. Honoured by the driver layer; the underlying
|
||||
/// libplctag 1.5.2 wrapper has no direct <c>ConnectionSize</c> property, so the value is
|
||||
/// plumbed through <see cref="AbCipTagCreateParams"/> for forward-compat with future wrapper
|
||||
/// versions or a custom tag-attribute path; current builds use the family profile default at
|
||||
/// the wire layer regardless.</param>
|
||||
/// profile's <c>DefaultConnectionSize</c>. The native transport honours it on the wire (its Forward
|
||||
/// Open size). The libplctag transport ignores it: the libplctag 1.5.2 wrapper has no
|
||||
/// <c>ConnectionSize</c> property, so libplctag opens with its own size for the <c>plc</c> attribute.</param>
|
||||
public sealed record AbCipDeviceOptions(
|
||||
string HostAddress,
|
||||
AbCipPlcFamily PlcFamily = AbCipPlcFamily.ControlLogix,
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using libplctag;
|
||||
using libplctag.NativeImport;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
|
||||
/// <summary>
|
||||
/// Releases the native tag a FAILED libplctag initialisation leaves behind (T21 press pass 1 crash).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>libplctag.NET 1.5.2's <c>NativeTagWrapper.InitializeAsync</c> calls <c>plc_tag_create_ex</c>, which returns a
|
||||
/// native handle with the wrapper's <c>callback_func_ex</c> delegate registered, and only then waits for the
|
||||
/// <c>Created</c> event. When that event carries an error (a tag the controller does not have, a refused path, ...)
|
||||
/// it throws WITHOUT setting <c>_isInitialized</c>, and <c>Dispose()</c> unregisters and destroys only
|
||||
/// <c>if (_isInitialized)</c>. So the native tag outlives its wrapper with the callback still registered; once the
|
||||
/// wrapper is collected the delegate's thunk is freed, and the next event libplctag raises on that tag — at the
|
||||
/// latest the <c>DESTROYED</c> event from <c>plc_tag_shutdown</c>, which the native library runs from
|
||||
/// <c>atexit</c> — kills the process with "A callback was made on a garbage collected delegate of type
|
||||
/// 'libplctag.NativeImport!libplctag.NativeImport.plctag+callback_func_ex::Invoke'". A timed-out initialisation
|
||||
/// unregisters the callback but never destroys the handle either, so that native tag keeps reconnecting in the
|
||||
/// background for the life of the process.</para>
|
||||
/// <para>The wrapper exposes no handle, so it is read by reflection from the two private fields of the pinned
|
||||
/// 1.5.2 wrapper (<c>Tag._tag</c>, <c>NativeTagWrapper.nativeTagHandle</c>). If a future wrapper renames them the
|
||||
/// release cannot run; the tag is then rooted for the life of the process instead, which keeps its delegate
|
||||
/// alive. That is no crash but an UNBOUNDED leak — one native tag (holding its libplctag session open) per failed
|
||||
/// create, and the data path re-creates a NotFound tag on every read — so <c>LibplctagOrphanReleaseTests</c> fails, so the bump is noticed.</para>
|
||||
/// </remarks>
|
||||
internal static class LibplctagOrphanRelease
|
||||
{
|
||||
private const BindingFlags Private = BindingFlags.NonPublic | BindingFlags.Instance;
|
||||
|
||||
private static readonly FieldInfo? WrapperField = typeof(Tag).GetField("_tag", Private);
|
||||
private static readonly FieldInfo? HandleField = WrapperField?.FieldType.GetField("nativeTagHandle", Private);
|
||||
|
||||
/// <summary>Tags whose handle could not be read: kept reachable so their callback delegate is never collected.</summary>
|
||||
private static readonly ConcurrentBag<Tag> Rooted = [];
|
||||
|
||||
private static int _released;
|
||||
|
||||
/// <summary>True when the pinned wrapper's private fields were found, so a failed tag can be destroyed.</summary>
|
||||
internal static bool CanReadHandles => HandleField is not null && !ForceRootingForTest;
|
||||
|
||||
/// <summary>Test seam (review r1 M1): take the rooting fallback as if the wrapper's fields had been renamed.</summary>
|
||||
internal static bool ForceRootingForTest { get; set; }
|
||||
|
||||
/// <summary>Orphaned native tags destroyed so far (process-wide; diagnostics and tests).</summary>
|
||||
internal static int Released => Volatile.Read(ref _released);
|
||||
|
||||
/// <summary>Orphaned tags rooted for the process lifetime because their handle could not be read.</summary>
|
||||
internal static int RootedCount => Rooted.Count;
|
||||
|
||||
/// <summary>The native handle the wrapper holds (0 when none was ever created, or unreadable).</summary>
|
||||
internal static int HandleOf(Tag tag) =>
|
||||
HandleField is not null && WrapperField!.GetValue(tag) is { } wrapper ? (int)HandleField.GetValue(wrapper)! : 0;
|
||||
|
||||
/// <summary>Clears the wrapper's handle after a destroy, so a repeat release is a no-op (review r1 M2).</summary>
|
||||
private static void ClearHandle(Tag tag)
|
||||
{
|
||||
if (HandleField is not null && WrapperField!.GetValue(tag) is { } wrapper) HandleField.SetValue(wrapper, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a libplctag operation that may initialise <paramref name="tag"/> (InitializeAsync, or a first
|
||||
/// Read/Write, which initialise on demand). If it fails and leaves the tag uninitialised, the native tag it
|
||||
/// created is unregistered and destroyed before the failure propagates.
|
||||
/// </summary>
|
||||
internal static async Task RunAsync(Tag tag, Func<Task> op)
|
||||
{
|
||||
try
|
||||
{
|
||||
await op().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ReleaseIfOrphaned(tag);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="RunAsync(Tag, Func{Task})"/>
|
||||
internal static async Task<T> RunAsync<T>(Tag tag, Func<Task<T>> op)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await op().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ReleaseIfOrphaned(tag);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destroys the native tag behind an UNINITIALISED wrapper, unregistering its callback first. No-op for an
|
||||
/// initialised tag (its own <c>Dispose</c> handles it) or one that never got a handle. Never throws.
|
||||
/// </summary>
|
||||
/// <returns>The status the native tag held just before it was destroyed (what <c>GetStatus()</c> would have
|
||||
/// reported), or null when nothing was destroyed.</returns>
|
||||
internal static Status? ReleaseIfOrphaned(Tag tag)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (tag.IsInitialized) return null;
|
||||
if (!CanReadHandles)
|
||||
{
|
||||
Rooted.Add(tag);
|
||||
return null;
|
||||
}
|
||||
var handle = HandleOf(tag);
|
||||
if (handle <= 0) return null; // plc_tag_create_ex refused synchronously: nothing native exists
|
||||
var status = (Status)plctag.plc_tag_status(handle);
|
||||
plctag.plc_tag_unregister_callback(handle);
|
||||
plctag.plc_tag_destroy(handle);
|
||||
ClearHandle(tag);
|
||||
Interlocked.Increment(ref _released);
|
||||
return status;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// A release must never mask the initialisation failure it follows. Root the tag so that, whatever
|
||||
// state the native side is in, its callback delegate stays alive.
|
||||
Rooted.Add(tag);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,13 @@ internal static class LibplctagListingReader
|
||||
AbCipTagCreateParams deviceParams, string listingName, CancellationToken cancellationToken)
|
||||
{
|
||||
using var tag = BuildSymbolListTag(deviceParams, listingName);
|
||||
return await LibplctagStatusTranslation.Wrap(async () =>
|
||||
// A failed initialisation would otherwise leave the native tag (and its callback) behind: see LibplctagOrphanRelease.
|
||||
return await LibplctagOrphanRelease.RunAsync(tag, () => LibplctagStatusTranslation.Wrap(async () =>
|
||||
{
|
||||
await tag.InitializeAsync(cancellationToken).ConfigureAwait(false);
|
||||
await tag.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
return tag.GetBuffer();
|
||||
}).ConfigureAwait(false);
|
||||
})).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -14,6 +14,15 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
|
||||
{
|
||||
private readonly Tag _tag;
|
||||
|
||||
/// <summary>
|
||||
/// The status the native tag held when a failed initialisation destroyed it (see
|
||||
/// <see cref="LibplctagOrphanRelease"/>); null while the wrapper's own handle is authoritative.
|
||||
/// </summary>
|
||||
private Status? _releasedStatus;
|
||||
|
||||
/// <summary>The native handle a failed initialisation destroyed (0 if none); the wrapper's own field is cleared.</summary>
|
||||
private int _releasedHandle;
|
||||
|
||||
/// <summary>Initializes a new instance of the LibplctagTagRuntime class.</summary>
|
||||
/// <param name="p">The tag creation parameters.</param>
|
||||
public LibplctagTagRuntime(AbCipTagCreateParams p)
|
||||
@@ -42,18 +51,52 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task InitializeAsync(CancellationToken cancellationToken) =>
|
||||
LibplctagStatusTranslation.Wrap(() => _tag.InitializeAsync(cancellationToken));
|
||||
RunReleasingOrphanAsync(() => _tag.InitializeAsync(cancellationToken));
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>libplctag initialises an uninitialised tag on its first read, so this path can orphan one too.</remarks>
|
||||
public Task ReadAsync(CancellationToken cancellationToken) =>
|
||||
LibplctagStatusTranslation.Wrap(() => _tag.ReadAsync(cancellationToken));
|
||||
RunReleasingOrphanAsync(() => _tag.ReadAsync(cancellationToken));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task WriteAsync(CancellationToken cancellationToken) =>
|
||||
LibplctagStatusTranslation.Wrap(() => _tag.WriteAsync(cancellationToken));
|
||||
RunReleasingOrphanAsync(() => _tag.WriteAsync(cancellationToken));
|
||||
|
||||
/// <summary>
|
||||
/// Runs a libplctag operation; when it fails and leaves the tag uninitialised, destroys the native tag it
|
||||
/// created before the failure propagates, so its callback can never fire into a collected delegate (T21 press
|
||||
/// pass 1 crash). The status that tag held is kept for <see cref="GetStatus"/>.
|
||||
/// </summary>
|
||||
private async Task RunReleasingOrphanAsync(Func<Task> op)
|
||||
{
|
||||
try
|
||||
{
|
||||
await LibplctagStatusTranslation.Wrap(op).ConfigureAwait(false);
|
||||
_releasedStatus = null;
|
||||
_releasedHandle = 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
var handle = LibplctagOrphanRelease.HandleOf(_tag);
|
||||
if (LibplctagOrphanRelease.ReleaseIfOrphaned(_tag) is { } status)
|
||||
{
|
||||
_releasedStatus = status;
|
||||
_releasedHandle = handle;
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The native libplctag handle this runtime's tag holds, or the one a failed initialisation destroyed
|
||||
/// (0 if none was created). Tests only.</summary>
|
||||
internal int NativeHandle => _releasedHandle != 0 ? _releasedHandle : LibplctagOrphanRelease.HandleOf(_tag);
|
||||
|
||||
/// <summary>The handle the libplctag wrapper itself still holds (0 once a release cleared it). Tests only.</summary>
|
||||
internal int WrapperHandle => LibplctagOrphanRelease.HandleOf(_tag);
|
||||
|
||||
/// <inheritdoc />
|
||||
public int GetStatus() => (int)LibplctagStatusTranslation.ToAbStatus(_tag.GetStatus());
|
||||
public int GetStatus() =>
|
||||
(int)LibplctagStatusTranslation.ToAbStatus(_releasedStatus ?? _tag.GetStatus());
|
||||
|
||||
/// <inheritdoc />
|
||||
public int GetSize() => LibplctagStatusTranslation.WrapSync(() => _tag.GetSize());
|
||||
|
||||
@@ -21,12 +21,13 @@ internal sealed class LibplctagTemplateReader : IAbCipTemplateReader
|
||||
_tag?.Dispose();
|
||||
_tag = BuildTemplateTag(deviceParams, templateInstanceId);
|
||||
var tag = _tag;
|
||||
return await LibplctagStatusTranslation.Wrap(async () =>
|
||||
// A failed initialisation would otherwise leave the native tag (and its callback) behind: see LibplctagOrphanRelease.
|
||||
return await LibplctagOrphanRelease.RunAsync(tag, () => LibplctagStatusTranslation.Wrap(async () =>
|
||||
{
|
||||
await tag.InitializeAsync(cancellationToken).ConfigureAwait(false);
|
||||
await tag.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
return tag.GetBuffer();
|
||||
}).ConfigureAwait(false);
|
||||
})).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>Builds the (uninitialized) libplctag tag for the <c>@udt/{id}</c> pseudo.</summary>
|
||||
|
||||
@@ -41,7 +41,12 @@ public sealed record AbCipPlcFamilyProfile(
|
||||
|
||||
public static readonly AbCipPlcFamilyProfile CompactLogix = new(
|
||||
LibplctagPlcAttribute: "compactlogix",
|
||||
DefaultConnectionSize: 504, // 5069-L3x narrower buffer; safe baseline that never over-shoots
|
||||
// Large Forward Open, matching libplctag: `plc=compactlogix` opens Large (0x5B, 4002) and the
|
||||
// native transport must not pack into a third of the window (T21 latency harness: 504 cost ~3x
|
||||
// the round trips per poll). A pre-v20 CompactLogix (1769-L3x) or a bridge that refuses the Large open
|
||||
// is covered by CipConnection's fallback — a regular open at min(size, 504) inside the same
|
||||
// throttled attempt. An explicit ConnectionSize (driver options or per device) still wins.
|
||||
DefaultConnectionSize: 4002,
|
||||
DefaultCipPath: "1,0",
|
||||
SupportsRequestPacking: true,
|
||||
SupportsConnectedMessaging: true,
|
||||
|
||||
@@ -13,6 +13,15 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
|
||||
{
|
||||
private readonly Tag _tag;
|
||||
|
||||
/// <summary>
|
||||
/// The status the native tag held when a failed initialisation destroyed it (see
|
||||
/// <see cref="LibplctagOrphanRelease"/>); null while the wrapper's own handle is authoritative.
|
||||
/// </summary>
|
||||
private Status? _releasedStatus;
|
||||
|
||||
/// <summary>The native handle a failed initialisation destroyed (0 if none); the wrapper's own field is cleared.</summary>
|
||||
private int _releasedHandle;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="LibplctagLegacyTagRuntime"/> class.</summary>
|
||||
/// <param name="p">The parameters for tag creation.</param>
|
||||
public LibplctagLegacyTagRuntime(AbLegacyTagCreateParams p)
|
||||
@@ -38,18 +47,52 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task InitializeAsync(CancellationToken cancellationToken) =>
|
||||
LibplctagLegacyStatusTranslation.Wrap(() => _tag.InitializeAsync(cancellationToken));
|
||||
RunReleasingOrphanAsync(() => _tag.InitializeAsync(cancellationToken));
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>libplctag initialises an uninitialised tag on its first read, so this path can orphan one too.</remarks>
|
||||
public Task ReadAsync(CancellationToken cancellationToken) =>
|
||||
LibplctagLegacyStatusTranslation.Wrap(() => _tag.ReadAsync(cancellationToken));
|
||||
RunReleasingOrphanAsync(() => _tag.ReadAsync(cancellationToken));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task WriteAsync(CancellationToken cancellationToken) =>
|
||||
LibplctagLegacyStatusTranslation.Wrap(() => _tag.WriteAsync(cancellationToken));
|
||||
RunReleasingOrphanAsync(() => _tag.WriteAsync(cancellationToken));
|
||||
|
||||
/// <summary>
|
||||
/// Runs a libplctag operation; when it fails and leaves the tag uninitialised, destroys the native tag it
|
||||
/// created before the failure propagates, so its callback can never fire into a collected delegate (T21 press
|
||||
/// pass 1 crash). The status that tag held is kept for <see cref="GetStatus"/>.
|
||||
/// </summary>
|
||||
private async Task RunReleasingOrphanAsync(Func<Task> op)
|
||||
{
|
||||
try
|
||||
{
|
||||
await LibplctagLegacyStatusTranslation.Wrap(op).ConfigureAwait(false);
|
||||
_releasedStatus = null;
|
||||
_releasedHandle = 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
var handle = LibplctagOrphanRelease.HandleOf(_tag);
|
||||
if (LibplctagOrphanRelease.ReleaseIfOrphaned(_tag) is { } status)
|
||||
{
|
||||
_releasedStatus = status;
|
||||
_releasedHandle = handle;
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The native libplctag handle this runtime's tag holds, or the one a failed initialisation destroyed
|
||||
/// (0 if none was created). Tests only.</summary>
|
||||
internal int NativeHandle => _releasedHandle != 0 ? _releasedHandle : LibplctagOrphanRelease.HandleOf(_tag);
|
||||
|
||||
/// <summary>The handle the libplctag wrapper itself still holds (0 once a release cleared it). Tests only.</summary>
|
||||
internal int WrapperHandle => LibplctagOrphanRelease.HandleOf(_tag);
|
||||
|
||||
/// <inheritdoc />
|
||||
public int GetStatus() => (int)LibplctagLegacyStatusTranslation.ToAbStatus(_tag.GetStatus());
|
||||
public int GetStatus() =>
|
||||
(int)LibplctagLegacyStatusTranslation.ToAbStatus(_releasedStatus ?? _tag.GetStatus());
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? DecodeValue(AbLegacyDataType type, int? bitIndex) => type switch
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using libplctag;
|
||||
using libplctag.NativeImport;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
||||
|
||||
/// <summary>
|
||||
/// Releases the native tag a FAILED libplctag initialisation leaves behind (T21 press pass 1 crash).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>libplctag.NET 1.5.2's <c>NativeTagWrapper.InitializeAsync</c> calls <c>plc_tag_create_ex</c>, which returns a
|
||||
/// native handle with the wrapper's <c>callback_func_ex</c> delegate registered, and only then waits for the
|
||||
/// <c>Created</c> event. When that event carries an error (a tag the controller does not have, a refused path, ...)
|
||||
/// it throws WITHOUT setting <c>_isInitialized</c>, and <c>Dispose()</c> unregisters and destroys only
|
||||
/// <c>if (_isInitialized)</c>. So the native tag outlives its wrapper with the callback still registered; once the
|
||||
/// wrapper is collected the delegate's thunk is freed, and the next event libplctag raises on that tag — at the
|
||||
/// latest the <c>DESTROYED</c> event from <c>plc_tag_shutdown</c>, which the native library runs from
|
||||
/// <c>atexit</c> — kills the process with "A callback was made on a garbage collected delegate of type
|
||||
/// 'libplctag.NativeImport!libplctag.NativeImport.plctag+callback_func_ex::Invoke'". A timed-out initialisation
|
||||
/// unregisters the callback but never destroys the handle either, so that native tag keeps reconnecting in the
|
||||
/// background for the life of the process.</para>
|
||||
/// <para>The wrapper exposes no handle, so it is read by reflection from the two private fields of the pinned
|
||||
/// 1.5.2 wrapper (<c>Tag._tag</c>, <c>NativeTagWrapper.nativeTagHandle</c>). If a future wrapper renames them the
|
||||
/// release cannot run; the tag is then rooted for the life of the process instead, which keeps its delegate
|
||||
/// alive. That is no crash but an UNBOUNDED leak — one native tag (holding its libplctag session open) per failed
|
||||
/// create, and the data path re-creates a NotFound tag on every read — so <c>LibplctagLegacyOrphanReleaseTests</c> fails, so the bump is noticed.</para>
|
||||
/// </remarks>
|
||||
internal static class LibplctagOrphanRelease
|
||||
{
|
||||
private const BindingFlags Private = BindingFlags.NonPublic | BindingFlags.Instance;
|
||||
|
||||
private static readonly FieldInfo? WrapperField = typeof(Tag).GetField("_tag", Private);
|
||||
private static readonly FieldInfo? HandleField = WrapperField?.FieldType.GetField("nativeTagHandle", Private);
|
||||
|
||||
/// <summary>Tags whose handle could not be read: kept reachable so their callback delegate is never collected.</summary>
|
||||
private static readonly ConcurrentBag<Tag> Rooted = [];
|
||||
|
||||
private static int _released;
|
||||
|
||||
/// <summary>True when the pinned wrapper's private fields were found, so a failed tag can be destroyed.</summary>
|
||||
internal static bool CanReadHandles => HandleField is not null && !ForceRootingForTest;
|
||||
|
||||
/// <summary>Test seam (review r1 M1): take the rooting fallback as if the wrapper's fields had been renamed.</summary>
|
||||
internal static bool ForceRootingForTest { get; set; }
|
||||
|
||||
/// <summary>Orphaned native tags destroyed so far (process-wide; diagnostics and tests).</summary>
|
||||
internal static int Released => Volatile.Read(ref _released);
|
||||
|
||||
/// <summary>Orphaned tags rooted for the process lifetime because their handle could not be read.</summary>
|
||||
internal static int RootedCount => Rooted.Count;
|
||||
|
||||
/// <summary>The native handle the wrapper holds (0 when none was ever created, or unreadable).</summary>
|
||||
internal static int HandleOf(Tag tag) =>
|
||||
HandleField is not null && WrapperField!.GetValue(tag) is { } wrapper ? (int)HandleField.GetValue(wrapper)! : 0;
|
||||
|
||||
/// <summary>Clears the wrapper's handle after a destroy, so a repeat release is a no-op (review r1 M2).</summary>
|
||||
private static void ClearHandle(Tag tag)
|
||||
{
|
||||
if (HandleField is not null && WrapperField!.GetValue(tag) is { } wrapper) HandleField.SetValue(wrapper, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a libplctag operation that may initialise <paramref name="tag"/> (InitializeAsync, or a first
|
||||
/// Read/Write, which initialise on demand). If it fails and leaves the tag uninitialised, the native tag it
|
||||
/// created is unregistered and destroyed before the failure propagates.
|
||||
/// </summary>
|
||||
internal static async Task RunAsync(Tag tag, Func<Task> op)
|
||||
{
|
||||
try
|
||||
{
|
||||
await op().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ReleaseIfOrphaned(tag);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="RunAsync(Tag, Func{Task})"/>
|
||||
internal static async Task<T> RunAsync<T>(Tag tag, Func<Task<T>> op)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await op().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ReleaseIfOrphaned(tag);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destroys the native tag behind an UNINITIALISED wrapper, unregistering its callback first. No-op for an
|
||||
/// initialised tag (its own <c>Dispose</c> handles it) or one that never got a handle. Never throws.
|
||||
/// </summary>
|
||||
/// <returns>The status the native tag held just before it was destroyed (what <c>GetStatus()</c> would have
|
||||
/// reported), or null when nothing was destroyed.</returns>
|
||||
internal static Status? ReleaseIfOrphaned(Tag tag)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (tag.IsInitialized) return null;
|
||||
if (!CanReadHandles)
|
||||
{
|
||||
Rooted.Add(tag);
|
||||
return null;
|
||||
}
|
||||
var handle = HandleOf(tag);
|
||||
if (handle <= 0) return null; // plc_tag_create_ex refused synchronously: nothing native exists
|
||||
var status = (Status)plctag.plc_tag_status(handle);
|
||||
plctag.plc_tag_unregister_callback(handle);
|
||||
plctag.plc_tag_destroy(handle);
|
||||
ClearHandle(tag);
|
||||
Interlocked.Increment(ref _released);
|
||||
return status;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// A release must never mask the initialisation failure it follows. Root the tag so that, whatever
|
||||
// state the native side is in, its callback delegate stays alive.
|
||||
Rooted.Add(tag);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ public static class KnownProfiles
|
||||
public static readonly AbServerProfile CompactLogix = new(
|
||||
Family: AbCipPlcFamily.CompactLogix,
|
||||
ComposeProfile: "compactlogix",
|
||||
Notes: "ab_server doesn't enforce the narrower ConnectionSize; driver-side profile caps it per PR 10.");
|
||||
Notes: "Large Forward Open (4002) like libplctag since T21; ab_server accepts any size, so the regular-open fallback is unit-tested only.");
|
||||
|
||||
/// <summary>Gets the Micro800 profile.</summary>
|
||||
public static readonly AbServerProfile Micro800 = new(
|
||||
|
||||
+168
-28
@@ -11,7 +11,10 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential;
|
||||
internal sealed record SelectedTag(string Name, AbCipDataType? Type, int ElementCount, bool IsArray, string Origin);
|
||||
|
||||
/// <summary>What one transport observed for one tag.</summary>
|
||||
internal sealed record ReadObservation(int Status, string? Error, byte[]? Raw, object? Decoded, string? DecodeError = null)
|
||||
/// <param name="TypeCode">The CIP type code the reply carried, where the transport exposes it (native only: libplctag.NET
|
||||
/// 1.5.2 does not surface it).</param>
|
||||
internal sealed record ReadObservation(
|
||||
int Status, string? Error, byte[]? Raw, object? Decoded, string? DecodeError = null, ushort? TypeCode = null)
|
||||
{
|
||||
public bool Succeeded => Status == (int)AbStatus.Ok && Error is null;
|
||||
}
|
||||
@@ -27,6 +30,13 @@ internal sealed class TransportObservations
|
||||
/// <summary>Template ids that were queued but not read because <c>AB_DIFF_MAX_TEMPLATES</c> was reached.</summary>
|
||||
public int TemplatesTruncated { get; set; }
|
||||
public Dictionary<string, ReadObservation> Reads { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// libplctag only: a SECOND paced read of every selected tag, taken after the first pass and before native
|
||||
/// starts (T21 press pass 1). A tag whose two reads differ is live — its value moves on its own — and is
|
||||
/// compared with native structurally, not by value. Empty on the native side.
|
||||
/// </summary>
|
||||
public Dictionary<string, ReadObservation> ProbeReads { get; } = new(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>The outcome of one whole differential run.</summary>
|
||||
@@ -40,12 +50,27 @@ internal sealed class DifferentialResult
|
||||
/// <summary>Native Forward Opens counted between the counter reset and the anchor release.</summary>
|
||||
public required int NativeForwardOpens { get; init; }
|
||||
|
||||
/// <summary>Forward Close frames the native side put on the wire (sent) and got a reply to (acked), over the
|
||||
/// same window as <see cref="NativeForwardOpens"/> — the anchor's release included (T21 review r1 M9).</summary>
|
||||
public required NativeCloses NativeForwardCloses { get; init; }
|
||||
|
||||
/// <summary>TCP connections to the endpoint once the native session was released and drained (null when unobservable).</summary>
|
||||
public required int? ConnectionsAfterNative { get; init; }
|
||||
|
||||
/// <summary>TCP connections to the endpoint before libplctag started, and once its handles were disposed.</summary>
|
||||
public required int? ConnectionsBaseline { get; init; }
|
||||
public required int? ConnectionsBeforeNative { get; init; }
|
||||
public required TimeSpan LibplctagDrainWait { get; init; }
|
||||
|
||||
/// <summary>The endpoint's resolved addresses, and this machine's connections to it before the run (null when
|
||||
/// unobservable). Reused by the latency comparison's barriers, which must not start alongside a session.</summary>
|
||||
public required IReadOnlySet<IPAddress> Addresses { get; init; }
|
||||
public required IReadOnlySet<IPEndPoint>? BaselineEndpoints { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Forward Close frames the native side sent, and how many of them got a reply within their 1 s budget.</summary>
|
||||
internal readonly record struct NativeCloses(int Sent, int Acked);
|
||||
|
||||
/// <summary>
|
||||
/// The run stopped at its first connection-level failure, or when the native side opened more than once.
|
||||
/// Thrown, never swallowed: every live test then fails with this message.
|
||||
@@ -98,11 +123,14 @@ internal static class DifferentialRun
|
||||
|
||||
// ---- Barrier: libplctag's session must be gone before the native side connects. ----
|
||||
var drain = Stopwatch.StartNew();
|
||||
var beforeNative = await WaitForLibplctagSessionToCloseAsync(target, addresses, baseline, ct).ConfigureAwait(false);
|
||||
var beforeNative = await WaitForSessionsToCloseAsync(target, addresses, baseline, "libplctag", ct).ConfigureAwait(false);
|
||||
drain.Stop();
|
||||
|
||||
// ---- Phase 2: native only, every operation sharing ONE connection held open by the anchor. ----
|
||||
var (native, opens) = await RunNativePhaseAsync(target, selection, ct).ConfigureAwait(false);
|
||||
var (native, opens, closes) = await RunNativePhaseAsync(target, selection, ct).ConfigureAwait(false);
|
||||
|
||||
// ---- Barrier: the native session must be gone too (its Forward Close is awaited in the anchor's release). ----
|
||||
var afterNative = await WaitForSessionsToCloseAsync(target, addresses, baseline, "native", ct).ConfigureAwait(false);
|
||||
|
||||
return new DifferentialResult
|
||||
{
|
||||
@@ -111,9 +139,13 @@ internal static class DifferentialRun
|
||||
Native = native,
|
||||
Selection = selection,
|
||||
NativeForwardOpens = opens,
|
||||
NativeForwardCloses = closes,
|
||||
ConnectionsAfterNative = afterNative,
|
||||
ConnectionsBaseline = baseline?.Count,
|
||||
ConnectionsBeforeNative = beforeNative,
|
||||
LibplctagDrainWait = drain.Elapsed,
|
||||
Addresses = addresses,
|
||||
BaselineEndpoints = baseline,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -123,35 +155,76 @@ internal static class DifferentialRun
|
||||
/// a fresh session per tag; an anchor handle on the listing pseudo-tag, held for the whole phase, keeps
|
||||
/// every libplctag operation on ONE session — the mirror of the native anchor lease.
|
||||
/// </summary>
|
||||
internal static async Task<(TransportObservations Observations, IReadOnlyList<SelectedTag> Selection)> RunLibplctagPhaseAsync(
|
||||
internal static Task<(TransportObservations Observations, IReadOnlyList<SelectedTag> Selection)> RunLibplctagPhaseAsync(
|
||||
DifferentialTarget target, CancellationToken ct)
|
||||
{
|
||||
const string phase = "libplctag";
|
||||
var lib = new TransportObservations();
|
||||
var factories = AbCipTransportFactories.For(AbCipTransport.Libplctag);
|
||||
using var anchor = factories.Tags.Create(target.Params("@tags"));
|
||||
return WithLibplctagAnchorAsync(phase, target, "@tags", async () =>
|
||||
{
|
||||
await BrowseAsync(phase, target, factories.Enumerators, lib, afterEach: null, ct).ConfigureAwait(false);
|
||||
await ReadTemplatesAsync(phase, target, factories.Templates, lib, afterEach: null, ct).ConfigureAwait(false);
|
||||
var selection = Select(target, lib);
|
||||
await ReadTagsAsync(phase, target, factories.Tags, selection, lib.Reads, afterEach: null, ct).ConfigureAwait(false);
|
||||
// Volatility probe: read every tag again, paced, so a value that moves on its own (a live analog) is
|
||||
// recognised as live rather than reported as a transport divergence when native reads it later.
|
||||
await ReadTagsAsync(phase, target, factories.Tags, selection, lib.ProbeReads, afterEach: null, ct).ConfigureAwait(false);
|
||||
return ((TransportObservations)lib, selection);
|
||||
}, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds one libplctag handle on <paramref name="anchorTag"/> for the whole of <paramref name="body"/>, so every
|
||||
/// libplctag operation in it shares ONE session and ONE Forward Open. A failure to create the anchor aborts
|
||||
/// before <paramref name="body"/> runs; the anchor is disposed when <paramref name="body"/> ends, either way.
|
||||
/// Shared by the differential (anchor <c>@tags</c>) and the latency comparison (anchor = its first tag).
|
||||
/// </summary>
|
||||
internal static async Task<T> WithLibplctagAnchorAsync<T>(
|
||||
string phase, DifferentialTarget target, string anchorTag, Func<Task<T>> body, CancellationToken ct)
|
||||
{
|
||||
var factories = AbCipTransportFactories.For(AbCipTransport.Libplctag);
|
||||
using var anchor = factories.Tags.Create(target.Params(anchorTag));
|
||||
try
|
||||
{
|
||||
await anchor.InitializeAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException || !ct.IsCancellationRequested)
|
||||
{
|
||||
throw Abort(phase, "anchor session (@tags)", ex);
|
||||
throw Abort(phase, $"anchor session ({anchorTag})", ex);
|
||||
}
|
||||
await BrowseAsync(phase, target, factories.Enumerators, lib, afterEach: null, ct).ConfigureAwait(false);
|
||||
await ReadTemplatesAsync(phase, target, factories.Templates, lib, afterEach: null, ct).ConfigureAwait(false);
|
||||
var selection = Select(target, lib);
|
||||
await ReadTagsAsync(phase, target, factories.Tags, selection, lib, afterEach: null, ct).ConfigureAwait(false);
|
||||
return (lib, selection);
|
||||
return await body().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>The native phase alone; internal so the offline abort test can drive it against a fake endpoint.</summary>
|
||||
internal static async Task<(TransportObservations Observations, int ForwardOpens)> RunNativePhaseAsync(
|
||||
internal static async Task<(TransportObservations Observations, int ForwardOpens, NativeCloses ForwardCloses)> RunNativePhaseAsync(
|
||||
DifferentialTarget target, IReadOnlyList<SelectedTag> selection, CancellationToken ct)
|
||||
{
|
||||
const string phase = "native";
|
||||
var native = new TransportObservations();
|
||||
var factories = AbCipTransportFactories.For(AbCipTransport.Native);
|
||||
var (_, forwardOpens, forwardCloses) = await WithNativeAnchorAsync(phase, target, async checkOpens =>
|
||||
{
|
||||
await BrowseAsync(phase, target, factories.Enumerators, native, checkOpens, ct).ConfigureAwait(false);
|
||||
await ReadTemplatesAsync(phase, target, factories.Templates, native, checkOpens, ct).ConfigureAwait(false);
|
||||
await ReadTagsAsync(phase, target, factories.Tags, selection, native.Reads, checkOpens, ct).ConfigureAwait(false);
|
||||
return true;
|
||||
}).ConfigureAwait(false);
|
||||
return (native, forwardOpens, forwardCloses);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs <paramref name="body"/> with every native operation sharing ONE connection, held open by an anchor
|
||||
/// lease: the process-wide Forward Open counter is reset, the open-attempt budget is set to exactly one
|
||||
/// more attempt, and <paramref name="body"/> receives a check to call after each operation that aborts the
|
||||
/// moment a second Forward Open reaches the wire. The anchor is released and the budget restored in
|
||||
/// <c>finally</c>. Shared by the differential and the latency comparison.
|
||||
/// </summary>
|
||||
/// <returns>The body's result, the Forward Opens counted between the reset and the anchor release, and the
|
||||
/// Forward Closes counted between the reset and the END of the anchor release (which awaits the close).</returns>
|
||||
internal static async Task<(T Result, int ForwardOpens, NativeCloses ForwardCloses)> WithNativeAnchorAsync<T>(
|
||||
string phase, DifferentialTarget target, Func<Action<string>, Task<T>> body)
|
||||
{
|
||||
var anchorParams = target.Params("@tags");
|
||||
|
||||
void CheckOpens(string operation)
|
||||
@@ -164,28 +237,33 @@ internal static class DifferentialRun
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref CipConnection.ForwardOpenCount, 0);
|
||||
Interlocked.Exchange(ref CipConnection.ForwardClosesSent, 0);
|
||||
Interlocked.Exchange(ref CipConnection.ForwardClosesAcked, 0);
|
||||
Volatile.Write(ref CipConnection.OpenAttemptBudget, Volatile.Read(ref CipConnection.OpenAttempts) + 1);
|
||||
T result;
|
||||
int forwardOpens;
|
||||
NativeCloses forwardCloses;
|
||||
try
|
||||
{
|
||||
// Acquire does no I/O; the key and options carry no tag-specific field, so every lease shares this entry.
|
||||
var anchor = DeviceSessionPool.Shared.Acquire(NativeAbCipDevice.Key(anchorParams), NativeAbCipDevice.Options(anchorParams));
|
||||
try
|
||||
{
|
||||
await BrowseAsync(phase, target, factories.Enumerators, native, CheckOpens, ct).ConfigureAwait(false);
|
||||
await ReadTemplatesAsync(phase, target, factories.Templates, native, CheckOpens, ct).ConfigureAwait(false);
|
||||
await ReadTagsAsync(phase, target, factories.Tags, selection, native, CheckOpens, ct).ConfigureAwait(false);
|
||||
result = await body(CheckOpens).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
forwardOpens = Volatile.Read(ref CipConnection.ForwardOpenCount);
|
||||
await anchor.DisposeAsync().ConfigureAwait(false);
|
||||
forwardCloses = new NativeCloses(
|
||||
Volatile.Read(ref CipConnection.ForwardClosesSent), Volatile.Read(ref CipConnection.ForwardClosesAcked));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Volatile.Write(ref CipConnection.OpenAttemptBudget, -1);
|
||||
}
|
||||
return (native, forwardOpens);
|
||||
return (result, forwardOpens, forwardCloses);
|
||||
}
|
||||
|
||||
private static async Task BrowseAsync(
|
||||
@@ -357,7 +435,7 @@ internal static class DifferentialRun
|
||||
|
||||
private static async Task ReadTagsAsync(
|
||||
string phase, DifferentialTarget target, IAbCipTagFactory factory, IReadOnlyList<SelectedTag> selection,
|
||||
TransportObservations into, Action<string>? afterEach, CancellationToken ct)
|
||||
Dictionary<string, ReadObservation> into, Action<string>? afterEach, CancellationToken ct)
|
||||
{
|
||||
foreach (var tag in selection)
|
||||
{
|
||||
@@ -396,7 +474,8 @@ internal static class DifferentialRun
|
||||
decodeError = Describe(ex); // recorded, and a NO verdict (review r1 M3)
|
||||
}
|
||||
}
|
||||
into.Reads[tag.Name] = new ReadObservation(status, error, raw, decoded, decodeError);
|
||||
into[tag.Name] = new ReadObservation(status, error, raw, decoded, decodeError,
|
||||
(runtime as NativeAbCipTagRuntime)?.TypeCode);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -422,11 +501,11 @@ internal static class DifferentialRun
|
||||
_ => true,
|
||||
};
|
||||
|
||||
private static DifferentialAbortedException Abort(string phase, string operation, Exception ex) =>
|
||||
internal static DifferentialAbortedException Abort(string phase, string operation, Exception ex) =>
|
||||
new($"Differential ABORTED in the {phase} phase at {operation}: {Describe(ex)}. " +
|
||||
"Stopped on the first connection-level failure; nothing was retried.", ex);
|
||||
|
||||
private static Task PaceAsync(DifferentialTarget target, CancellationToken ct) =>
|
||||
internal static Task PaceAsync(DifferentialTarget target, CancellationToken ct) =>
|
||||
target.PaceMs > 0 ? Task.Delay(target.PaceMs, ct) : Task.CompletedTask;
|
||||
|
||||
/// <summary>Resolves the endpoint once, bounded (an IP literal needs no lookup).</summary>
|
||||
@@ -448,14 +527,16 @@ internal static class DifferentialRun
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// libplctag closes its session on a background thread some time after the last handle is destroyed.
|
||||
/// Wait until no TCP connection to the endpoint remains that was not already there before the run
|
||||
/// (compared by local endpoint, not by count — review r1 M6), so the two transports are never
|
||||
/// A transport closes its session asynchronously (libplctag on a background thread some time after the last
|
||||
/// handle is destroyed). Wait until no TCP connection to the endpoint remains that was not already there
|
||||
/// before the run (compared by local endpoint, not by count — review r1 M6), so two sessions are never
|
||||
/// connected to the controller at the same time.
|
||||
/// </summary>
|
||||
/// <returns>The connection count observed when native work may start (null when unobservable).</returns>
|
||||
private static async Task<int?> WaitForLibplctagSessionToCloseAsync(
|
||||
DifferentialTarget target, IReadOnlySet<IPAddress> addresses, IReadOnlySet<IPEndPoint>? baseline, CancellationToken ct)
|
||||
/// <param name="whose">Names the session waited for, in the abort message.</param>
|
||||
/// <returns>The connection count observed when the next phase may start (null when unobservable).</returns>
|
||||
internal static async Task<int?> WaitForSessionsToCloseAsync(
|
||||
DifferentialTarget target, IReadOnlySet<IPAddress> addresses, IReadOnlySet<IPEndPoint>? baseline, string whose,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (baseline is null)
|
||||
{
|
||||
@@ -471,8 +552,8 @@ internal static class DifferentialRun
|
||||
if (added.Count == 0) return now.Count;
|
||||
if (deadline.Elapsed > LibplctagDrainBudget)
|
||||
throw new DifferentialAbortedException(
|
||||
$"libplctag's session to {target.Host}:{target.Port} was still open {LibplctagDrainBudget.TotalSeconds:0}s after its " +
|
||||
$"handles were disposed (local {string.Join(", ", added)}); refusing to start the native phase alongside it.");
|
||||
$"{whose}'s session to {target.Host}:{target.Port} was still open {LibplctagDrainBudget.TotalSeconds:0}s after it was " +
|
||||
$"released (local {string.Join(", ", added)}); refusing to start the next phase alongside it.");
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(250), ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -514,6 +595,65 @@ internal static class DifferentialRun
|
||||
? StructuralComparisons.StructuralEqualityComparer.Equals(a, b)
|
||||
: Equals(a, b);
|
||||
|
||||
/// <summary>
|
||||
/// True when the libplctag volatility probe saw the value move by itself: both libplctag reads succeeded and
|
||||
/// their bytes differ. A missing or failed probe is never live, so such a tag keeps the full value comparison.
|
||||
/// </summary>
|
||||
internal static bool IsLive(ReadObservation lib, ReadObservation? probe) =>
|
||||
probe is not null && lib.Succeeded && probe.Succeeded && lib.Raw is not null && probe.Raw is not null
|
||||
&& !lib.Raw.AsSpan().SequenceEqual(probe.Raw);
|
||||
|
||||
/// <summary>
|
||||
/// The value check that survives for a LIVE tag (T21 review r1 I1). A scalar is one element, an array element-wise
|
||||
/// over the decoded values:
|
||||
/// <list type="bullet">
|
||||
/// <item>an element EQUAL in both libplctag reads did not move, so native must read it equal too —
|
||||
/// otherwise <c>**NO: stable element i**</c> (a stride, offset or byte-order defect lands here);</item>
|
||||
/// <item>an element that moved is excused its value, but native must not read a NaN/∞ that neither
|
||||
/// libplctag read saw — <c>**NO: non-finite element i**</c>.</item>
|
||||
/// </list>
|
||||
/// Null means the check passed. Shapes must already agree between libplctag and native.
|
||||
/// </summary>
|
||||
internal static string? LiveElementMismatch(object? lib, object? probe, object? nat)
|
||||
{
|
||||
var l = Elements(lib);
|
||||
var p = Elements(probe);
|
||||
var n = Elements(nat);
|
||||
if (l.Length != p.Length) return "**NO: probe shape**"; // libplctag disagreed with itself: never excuse it
|
||||
if (l.Length != n.Length) return "**NO: shape**";
|
||||
for (var i = 0; i < l.Length; i++)
|
||||
{
|
||||
if (Equals(l[i], p[i]))
|
||||
{
|
||||
if (!Equals(l[i], n[i])) return $"**NO: stable element {i}**";
|
||||
}
|
||||
else if (!IsFinite(n[i]) && !Equals(n[i], l[i]) && !Equals(n[i], p[i]))
|
||||
{
|
||||
return $"**NO: non-finite element {i}**";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static object?[] Elements(object? v) => v is Array a ? a.Cast<object?>().ToArray() : [v];
|
||||
|
||||
private static bool IsFinite(object? v) => v switch
|
||||
{
|
||||
float f => float.IsFinite(f),
|
||||
double d => double.IsFinite(d),
|
||||
_ => true,
|
||||
};
|
||||
|
||||
/// <summary>The same decoded shape: both scalars of one CLR type, or arrays of one element type and length.</summary>
|
||||
internal static bool SameShape(object? a, object? b) => (a, b) switch
|
||||
{
|
||||
(null, null) => true,
|
||||
(Array x, Array y) => x.GetType() == y.GetType() && x.Length == y.Length,
|
||||
(Array, _) or (_, Array) => false,
|
||||
(not null, not null) => a.GetType() == b.GetType(),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
/// <summary>Equal only when BOTH decoded: an undecodable shape is never parity (review r1 I3).</summary>
|
||||
internal static bool ShapesEqual(AbCipUdtShape? a, AbCipUdtShape? b) =>
|
||||
a is not null && b is not null
|
||||
|
||||
+22
-2
@@ -102,7 +102,8 @@ public sealed class DifferentialSafetyTests
|
||||
[Fact]
|
||||
public void Volume_knobs_fail_fast_above_the_ceiling_instead_of_clamping()
|
||||
{
|
||||
var saved = Snapshot(DifferentialTarget.EndpointVar, DifferentialTarget.MaxTagsVar, DifferentialTarget.MaxTemplatesVar, DifferentialTarget.PaceVar);
|
||||
var saved = Snapshot(DifferentialTarget.EndpointVar, DifferentialTarget.MaxTagsVar, DifferentialTarget.MaxTemplatesVar, DifferentialTarget.PaceVar,
|
||||
DifferentialTarget.LatencyTagsVar, DifferentialTarget.LatencyPassesVar, DifferentialTarget.LatencyPassGapVar);
|
||||
try
|
||||
{
|
||||
Environment.SetEnvironmentVariable(DifferentialTarget.EndpointVar, "127.0.0.1:1");
|
||||
@@ -115,10 +116,29 @@ public sealed class DifferentialSafetyTests
|
||||
|
||||
Environment.SetEnvironmentVariable(DifferentialTarget.MaxTemplatesVar, null);
|
||||
Environment.SetEnvironmentVariable(DifferentialTarget.PaceVar, null);
|
||||
var t = DifferentialTarget.FromEnvironment()!;
|
||||
Environment.SetEnvironmentVariable(DifferentialTarget.LatencyTagsVar, null);
|
||||
Environment.SetEnvironmentVariable(DifferentialTarget.LatencyPassesVar, null);
|
||||
Environment.SetEnvironmentVariable(DifferentialTarget.LatencyPassGapVar, null);
|
||||
var t =DifferentialTarget.FromEnvironment()!;
|
||||
t.MaxTags.ShouldBe(500);
|
||||
t.MaxTemplates.ShouldBe(DifferentialTarget.DefaultMaxTemplates);
|
||||
t.PaceMs.ShouldBe(DifferentialTarget.DefaultPaceMs);
|
||||
t.LatencyTags.ShouldBe(100);
|
||||
t.LatencyPasses.ShouldBe(30);
|
||||
t.LatencyPassGapMs.ShouldBe(200);
|
||||
|
||||
// Latency knobs (Task 21): the same fail-fast rule, with tighter ceilings.
|
||||
Environment.SetEnvironmentVariable(DifferentialTarget.LatencyTagsVar, "201");
|
||||
Should.Throw<InvalidOperationException>(DifferentialTarget.FromEnvironment).Message.ShouldContain(DifferentialTarget.LatencyTagsVar);
|
||||
Environment.SetEnvironmentVariable(DifferentialTarget.LatencyTagsVar, "200");
|
||||
Environment.SetEnvironmentVariable(DifferentialTarget.LatencyPassesVar, "51");
|
||||
Should.Throw<InvalidOperationException>(DifferentialTarget.FromEnvironment).Message.ShouldContain(DifferentialTarget.LatencyPassesVar);
|
||||
Environment.SetEnvironmentVariable(DifferentialTarget.LatencyPassesVar, "50");
|
||||
Environment.SetEnvironmentVariable(DifferentialTarget.LatencyPassGapVar, "10001");
|
||||
Should.Throw<InvalidOperationException>(DifferentialTarget.FromEnvironment).Message.ShouldContain(DifferentialTarget.LatencyPassGapVar);
|
||||
Environment.SetEnvironmentVariable(DifferentialTarget.LatencyPassGapVar, "0");
|
||||
var lat = DifferentialTarget.FromEnvironment()!;
|
||||
(lat.LatencyTags, lat.LatencyPasses, lat.LatencyPassGapMs).ShouldBe((200, 50, 0));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
+23
-2
@@ -14,6 +14,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential;
|
||||
/// <item><c>AB_DIFF_MAX_TEMPLATES</c> — cap on template reads per transport, default 50, at most 500.</item>
|
||||
/// <item><c>AB_DIFF_PACE_MS</c> — delay before every harness-issued request, default 20 ms; 0 for ab_server.</item>
|
||||
/// <item><c>AB_DIFF_COUNT_OPENS</c> — <c>1</c> ⇒ assert exactly one native Forward Open over the run.</item>
|
||||
/// <item><c>AB_LAT_TAGS</c> — latency comparison: tags read per pass, default 100, at most 200 (higher fails fast).</item>
|
||||
/// <item><c>AB_LAT_PASSES</c> — latency comparison: passes per transport, default 30, at most 50.</item>
|
||||
/// <item><c>AB_LAT_PASS_GAP_MS</c> — latency comparison: pause between passes, default 200 ms, at most 10000.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
internal sealed record DifferentialTarget(
|
||||
@@ -25,7 +28,10 @@ internal sealed record DifferentialTarget(
|
||||
int MaxTags,
|
||||
bool CountOpens,
|
||||
int MaxTemplates = DifferentialTarget.DefaultMaxTemplates,
|
||||
int PaceMs = DifferentialTarget.DefaultPaceMs)
|
||||
int PaceMs = DifferentialTarget.DefaultPaceMs,
|
||||
int LatencyTags = DifferentialTarget.DefaultLatencyTags,
|
||||
int LatencyPasses = DifferentialTarget.DefaultLatencyPasses,
|
||||
int LatencyPassGapMs = DifferentialTarget.DefaultLatencyPassGapMs)
|
||||
{
|
||||
public const string EndpointVar = "AB_DIFF_ENDPOINT";
|
||||
public const string PathVar = "AB_DIFF_PATH";
|
||||
@@ -35,6 +41,16 @@ internal sealed record DifferentialTarget(
|
||||
public const string CountOpensVar = "AB_DIFF_COUNT_OPENS";
|
||||
public const string MaxTemplatesVar = "AB_DIFF_MAX_TEMPLATES";
|
||||
public const string PaceVar = "AB_DIFF_PACE_MS";
|
||||
public const string LatencyTagsVar = "AB_LAT_TAGS";
|
||||
public const string LatencyPassesVar = "AB_LAT_PASSES";
|
||||
public const string LatencyPassGapVar = "AB_LAT_PASS_GAP_MS";
|
||||
|
||||
/// <summary>Latency comparison ceilings: a production press is the target, and each pass is one burst.</summary>
|
||||
public const int LatencyTagsCeiling = 200;
|
||||
public const int LatencyPassesCeiling = 50;
|
||||
public const int DefaultLatencyTags = 100;
|
||||
public const int DefaultLatencyPasses = 30;
|
||||
public const int DefaultLatencyPassGapMs = 200;
|
||||
|
||||
/// <summary>Hard ceiling on <c>AB_DIFF_MAX_TAGS</c> / <c>AB_DIFF_MAX_TEMPLATES</c>: a production press is the target.</summary>
|
||||
public const int Ceiling = 500;
|
||||
@@ -86,8 +102,13 @@ internal sealed record DifferentialTarget(
|
||||
if (tags is { Count: > Ceiling })
|
||||
throw new InvalidOperationException($"{TagsVar} names {tags.Count} tags; the ceiling is {Ceiling}.");
|
||||
|
||||
var latencyTags = BoundedInt(LatencyTagsVar, DefaultLatencyTags, 1, LatencyTagsCeiling);
|
||||
var latencyPasses = BoundedInt(LatencyPassesVar, DefaultLatencyPasses, 1, LatencyPassesCeiling);
|
||||
var latencyGap = BoundedInt(LatencyPassGapVar, DefaultLatencyPassGapMs, 0, 10_000);
|
||||
|
||||
var countOpens = Environment.GetEnvironmentVariable(CountOpensVar) == "1";
|
||||
return new DifferentialTarget(host, port, path, family, tags, max, countOpens, maxTemplates, pace);
|
||||
return new DifferentialTarget(host, port, path, family, tags, max, countOpens, maxTemplates, pace,
|
||||
latencyTags, latencyPasses, latencyGap);
|
||||
}
|
||||
|
||||
/// <summary>Reads an integer knob; out of range FAILS rather than clamping, so a typo never silently widens a press run.</summary>
|
||||
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
using System.Globalization;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential;
|
||||
|
||||
/// <summary>
|
||||
/// Task 21 latency comparison: a fixed set of atomic tags polled through libplctag, then through the native
|
||||
/// transport, each pass issuing every read at once. Pass criterion: native p95 ≤ libplctag p95 × 1.10.
|
||||
/// <para>Gated by the same variables as the differential (<c>AB_DIFF_ENDPOINT</c> absent ⇒ skip), in the same
|
||||
/// serialized collection, and read-only under the same source/IL guard
|
||||
/// (<see cref="TransportDifferentialTests.The_harness_is_read_only_by_construction"/>). It runs AFTER the
|
||||
/// differential's one run, reusing its browse for tag selection, so the target sees one listing per transport,
|
||||
/// not two. Knobs: <c>AB_LAT_TAGS</c>, <c>AB_LAT_PASSES</c>, <c>AB_LAT_PASS_GAP_MS</c> (see
|
||||
/// <see cref="DifferentialTarget"/>).</para>
|
||||
/// </summary>
|
||||
[Collection(DifferentialCollection.Name)]
|
||||
[Trait("Category", "LiveIntegration")]
|
||||
public sealed class PollLatencyComparisonTests(DifferentialRunFixture fixture)
|
||||
{
|
||||
private const double Tolerance = 1.10;
|
||||
private static readonly TimeSpan RunBudget = TimeSpan.FromMinutes(10);
|
||||
|
||||
[Fact]
|
||||
public async Task Native_poll_p95_is_within_ten_percent_of_libplctag()
|
||||
{
|
||||
var target = DifferentialTarget.FromEnvironment();
|
||||
if (target is null) Assert.Skip(DifferentialTarget.SkipReason);
|
||||
var differential = await fixture.GetAsync();
|
||||
|
||||
using var cts = new CancellationTokenSource(RunBudget);
|
||||
var r = await PollLatencyRun.ExecuteAsync(target, differential, cts.Token);
|
||||
|
||||
Log($"## Latency — {r.Target}");
|
||||
Log($"Tags per pass: {r.Tags.Count} ({r.DistinctTags} distinct" +
|
||||
(r.Repeated ? $", REPEATED cyclically to reach {DifferentialTarget.LatencyTagsVar}={r.Target.LatencyTags}" : "") +
|
||||
$"); passes per transport: {r.Target.LatencyPasses}; gap between passes: {r.Target.LatencyPassGapMs} ms.");
|
||||
Log("Types: " + string.Join(", ", r.Tags.GroupBy(t => t.Type).Select(g => $"{g.Key} {g.Count()}")));
|
||||
if (r.Dropped.Count > 0) Log($"Dropped (libplctag refused at setup, removed from both sets): {string.Join(", ", r.Dropped)}");
|
||||
Log($"Native requests spent on setup (one read per tag): {r.NativeSetupRequests}; native Forward Opens: {r.NativeForwardOpens}; " +
|
||||
$"native Forward Closes sent/acked: {r.NativeForwardCloses.Sent}/{r.NativeForwardCloses.Acked}; " +
|
||||
$"connections after the native phase: {r.ConnectionsAfterNative?.ToString(CultureInfo.InvariantCulture) ?? "unobservable"}.");
|
||||
Log($"libplctag orphans destroyed so far: {LibplctagOrphanRelease.Released}; rooted (unreleasable): {LibplctagOrphanRelease.RootedCount}.");
|
||||
Log("");
|
||||
|
||||
var libP50 = PollLatencyRun.Percentile(r.Libplctag.Select(p => p.Wall), 50);
|
||||
var libP95 = PollLatencyRun.Percentile(r.Libplctag.Select(p => p.Wall), 95);
|
||||
var natP50 = PollLatencyRun.Percentile(r.Native.Select(p => p.Wall), 50);
|
||||
var natP95 = PollLatencyRun.Percentile(r.Native.Select(p => p.Wall), 95);
|
||||
var limit = libP95 * Tolerance;
|
||||
|
||||
Log("| Transport | Passes | p50 ms | p95 ms | min ms | max ms | CIP requests per pass (median) | Tag failures |");
|
||||
Log("|---|---|---|---|---|---|---|---|");
|
||||
Log(Row("libplctag", r.Libplctag, libP50, libP95));
|
||||
Log(Row("native", r.Native, natP50, natP95));
|
||||
Log("");
|
||||
Log($"Criterion: native p95 {Ms(natP95)} ms ≤ libplctag p95 {Ms(libP95)} ms × {Tolerance:0.00} = {Ms(limit)} ms → " +
|
||||
(natP95 <= limit ? "PASS" : "**FAIL**"));
|
||||
Log("");
|
||||
// Attribution (review r1 M8): GC collections during each pass (gen0/gen1/gen2) on both sides, and the
|
||||
// round trip of each native CIP exchange — a slow pass with fast exchanges is a client-side stall.
|
||||
Log("| Pass | libplctag ms | libplctag GC 0/1/2 | native ms | native GC 0/1/2 | native requests | native RTT min/mean/max ms |");
|
||||
Log("|---|---|---|---|---|---|---|");
|
||||
for (var i = 0; i < Math.Max(r.Libplctag.Count, r.Native.Count); i++)
|
||||
{
|
||||
var lib = i < r.Libplctag.Count ? r.Libplctag[i] : null;
|
||||
var nat = i < r.Native.Count ? r.Native[i] : null;
|
||||
Log($"| {i + 1} | {(lib is null ? "-" : Ms(lib.Wall.TotalMilliseconds))} | {lib?.Gc.ToString() ?? "-"} | " +
|
||||
$"{(nat is null ? "-" : Ms(nat.Wall.TotalMilliseconds))} | {nat?.Gc.ToString() ?? "-"} | " +
|
||||
$"{nat?.Requests?.ToString(CultureInfo.InvariantCulture) ?? "-"} | {Rtt(nat?.Rtt)} |");
|
||||
}
|
||||
foreach (var p in r.Problems) Log($"- PROBLEM: {p}");
|
||||
|
||||
r.Problems.ShouldBeEmpty();
|
||||
r.Libplctag.Count.ShouldBe(r.Target.LatencyPasses);
|
||||
r.Native.Count.ShouldBe(r.Target.LatencyPasses);
|
||||
r.Libplctag.Sum(p => p.TagFailures).ShouldBe(0, "every timed libplctag read must succeed");
|
||||
r.Native.Sum(p => p.TagFailures).ShouldBe(0, "every timed native read must succeed");
|
||||
if (r.Target.CountOpens)
|
||||
{
|
||||
r.NativeForwardOpens.ShouldBe(1);
|
||||
r.NativeForwardCloses.Sent.ShouldBe(1, "the native phase's one connection must be closed with exactly one Forward Close");
|
||||
}
|
||||
if (r.ConnectionsAfterNative is { } after && differential.ConnectionsBaseline is { } baseline)
|
||||
after.ShouldBeLessThanOrEqualTo(baseline, "the native session must be gone when the run reports");
|
||||
// Review r1 M4: the latency setup's refused libplctag tags are orphan candidates too — checked after them.
|
||||
LibplctagOrphanRelease.RootedCount.ShouldBe(0);
|
||||
natP95.ShouldBeLessThanOrEqualTo(limit, $"native p95 must be ≤ libplctag p95 × {Tolerance:0.00}");
|
||||
}
|
||||
|
||||
/// <summary>Offline: the tag pick takes atomic scalars only, and repeats a short set deliberately and visibly.</summary>
|
||||
[Fact]
|
||||
public void Tag_pick_takes_atomic_scalars_only_and_repeats_a_short_set_visibly()
|
||||
{
|
||||
var browsed = new TransportObservations();
|
||||
browsed.Browse.Add(new AbCipDiscoveredTag("D", null, AbCipDataType.DInt, false));
|
||||
browsed.Browse.Add(new AbCipDiscoveredTag("R", null, AbCipDataType.Real, false));
|
||||
browsed.Browse.Add(new AbCipDiscoveredTag("S", null, AbCipDataType.String, false));
|
||||
browsed.Browse.Add(new AbCipDiscoveredTag("T", null, AbCipDataType.Dt, false));
|
||||
browsed.Browse.Add(new AbCipDiscoveredTag("A", null, AbCipDataType.DInt, false, ElementCount: 4, IsArray: true));
|
||||
browsed.Browse.Add(new AbCipDiscoveredTag("Sys", null, AbCipDataType.DInt, false, IsSystemTag: true));
|
||||
var target = new DifferentialTarget("127.0.0.1", 1, "1,0", AbCipPlcFamily.CompactLogix, null, 200, false, LatencyTags: 5);
|
||||
|
||||
var (tags, distinct, repeated) = PollLatencyRun.PickTags(target, browsed);
|
||||
distinct.ShouldBe(2);
|
||||
repeated.ShouldBeTrue();
|
||||
tags.Select(t => t.Name).ShouldBe(["D", "R", "D", "R", "D"]);
|
||||
|
||||
var (one, oneDistinct, oneRepeated) = PollLatencyRun.PickTags(target with { LatencyTags = 1 }, browsed);
|
||||
one.ShouldHaveSingleItem().Name.ShouldBe("D");
|
||||
oneDistinct.ShouldBe(1);
|
||||
oneRepeated.ShouldBeFalse();
|
||||
|
||||
PollLatencyRun.Percentile([TimeSpan.FromMilliseconds(3), TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(2)], 50)
|
||||
.ShouldBe(2);
|
||||
PollLatencyRun.Percentile(Enumerable.Range(1, 20).Select(i => TimeSpan.FromMilliseconds(i)), 95).ShouldBe(19);
|
||||
}
|
||||
|
||||
private static string Row(string name, IReadOnlyList<LatencyPass> passes, double p50, double p95)
|
||||
{
|
||||
var walls = passes.Select(p => p.Wall.TotalMilliseconds).ToList();
|
||||
var requests = passes.Where(p => p.Requests is not null).Select(p => p.Requests!.Value).Order().ToList();
|
||||
var medianRequests = requests.Count == 0 ? "n/a" : requests[requests.Count / 2].ToString(CultureInfo.InvariantCulture);
|
||||
return $"| {name} | {passes.Count} | {Ms(p50)} | {Ms(p95)} | {Ms(walls.Min())} | {Ms(walls.Max())} | {medianRequests} | " +
|
||||
$"{passes.Sum(p => p.TagFailures)} |";
|
||||
}
|
||||
|
||||
private static string Rtt(ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip.CipConnection.ExchangeStats? s) =>
|
||||
s is not { Count: > 0 } x ? "-" : $"{Ms(x.MinMs)}/{Ms(x.MeanMs)}/{Ms(x.MaxMs)}";
|
||||
|
||||
private static string Ms(double ms) => ms.ToString("0.0", CultureInfo.InvariantCulture);
|
||||
|
||||
private static void Log(string line) => TestContext.Current.TestOutputHelper?.WriteLine(line);
|
||||
}
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
using System.Diagnostics;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential;
|
||||
|
||||
/// <summary>One timed burst: every tag read once, concurrently.</summary>
|
||||
/// <param name="Requests">Native only: <see cref="CipConnection.RequestsSent"/> delta over the pass (keep-alives included).</param>
|
||||
/// <param name="TagFailures">Reads that ended with a tag-level status (a connection-level one aborts instead).</param>
|
||||
/// <param name="Gc">Collections that ran during the pass, per generation (T21 review r1 M8: a client-side stall shows here).</param>
|
||||
/// <param name="Rtt">Native only: round trip of each CIP exchange in the pass, from <see cref="CipConnection"/> (a slow
|
||||
/// controller shows here; a slow pass with fast exchanges points at the client).</param>
|
||||
internal sealed record LatencyPass(
|
||||
int Index, TimeSpan Wall, int? Requests, int TagFailures, GcDelta Gc = default, CipConnection.ExchangeStats? Rtt = null);
|
||||
|
||||
/// <summary>GC collections per generation over a window (gen0 counts include gen1/gen2 collections, as the runtime reports).</summary>
|
||||
internal readonly record struct GcDelta(int Gen0, int Gen1, int Gen2)
|
||||
{
|
||||
public static GcDelta Now() => new(GC.CollectionCount(0), GC.CollectionCount(1), GC.CollectionCount(2));
|
||||
public static GcDelta operator -(GcDelta a, GcDelta b) => new(a.Gen0 - b.Gen0, a.Gen1 - b.Gen1, a.Gen2 - b.Gen2);
|
||||
public override string ToString() => $"{Gen0}/{Gen1}/{Gen2}";
|
||||
}
|
||||
|
||||
/// <summary>The outcome of the latency comparison.</summary>
|
||||
internal sealed class LatencyResult
|
||||
{
|
||||
public required DifferentialTarget Target { get; init; }
|
||||
|
||||
/// <summary>The fixed set read every pass, in order (a name appears more than once only when <see cref="Repeated"/>).</summary>
|
||||
public required IReadOnlyList<SelectedTag> Tags { get; init; }
|
||||
public required int DistinctTags { get; init; }
|
||||
public required bool Repeated { get; init; }
|
||||
|
||||
/// <summary>Names libplctag could not read at setup (tag-level status): dropped from BOTH transports' sets.</summary>
|
||||
public required IReadOnlyList<string> Dropped { get; init; }
|
||||
public required IReadOnlyList<LatencyPass> Libplctag { get; init; }
|
||||
public required IReadOnlyList<LatencyPass> Native { get; init; }
|
||||
public required int NativeForwardOpens { get; init; }
|
||||
|
||||
/// <summary>Forward Closes the native phase sent / got a reply to, its anchor release included (review r1 M9).</summary>
|
||||
public required NativeCloses NativeForwardCloses { get; init; }
|
||||
|
||||
/// <summary>TCP connections to the endpoint once the native phase's session was released and drained.</summary>
|
||||
public required int? ConnectionsAfterNative { get; init; }
|
||||
|
||||
/// <summary>Native requests spent creating the runtimes (one read each, before any timed pass).</summary>
|
||||
public required int NativeSetupRequests { get; init; }
|
||||
public required List<string> Problems { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Task 21's poll-latency comparison, read-only and built on the differential's machinery: the same
|
||||
/// endpoint parsing, the same anchors (one libplctag handle, one native lease), the same open-attempt budget,
|
||||
/// the same connection barrier and the same stop-on-first-connection-failure rule.
|
||||
/// <para>Order: the differential's run (its browse supplies the tags — no second listing), a barrier until its
|
||||
/// native session is gone, every libplctag pass with every handle then disposed, a barrier until libplctag's
|
||||
/// session is gone, then every native pass under one anchor lease. The two transports never overlap.</para>
|
||||
/// <para>Per transport: create and read every tag once (untimed, paced by <c>AB_DIFF_PACE_MS</c>), then
|
||||
/// <c>AB_LAT_PASSES</c> timed passes, each issuing all <c>AB_LAT_TAGS</c> reads at once (<c>Task.WhenAll</c>)
|
||||
/// so the native shared scheduler can pack them into Multiple Service Packets as libplctag packs internally,
|
||||
/// with <c>AB_LAT_PASS_GAP_MS</c> of quiet between passes.</para>
|
||||
/// </summary>
|
||||
internal static class PollLatencyRun
|
||||
{
|
||||
private static readonly HashSet<AbCipDataType> Atomic =
|
||||
[
|
||||
AbCipDataType.Bool, AbCipDataType.SInt, AbCipDataType.Int, AbCipDataType.DInt, AbCipDataType.LInt,
|
||||
AbCipDataType.USInt, AbCipDataType.UInt, AbCipDataType.UDInt, AbCipDataType.ULInt,
|
||||
AbCipDataType.Real, AbCipDataType.LReal,
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// The fixed tag set: atomic, non-array scalars, from the differential's libplctag browse — or, when
|
||||
/// <c>AB_DIFF_TAGS</c> is set, those of its names whose browsed type is an atomic scalar. When fewer
|
||||
/// distinct tags exist than <c>AB_LAT_TAGS</c>, the set is REPEATED cyclically up to the count (each
|
||||
/// repeat is its own runtime/handle on the same name) and reported as such — never silently.
|
||||
/// </summary>
|
||||
internal static (IReadOnlyList<SelectedTag> Tags, int Distinct, bool Repeated) PickTags(
|
||||
DifferentialTarget target, TransportObservations browsed)
|
||||
{
|
||||
var candidates = DifferentialRun.Select(target with { MaxTags = DifferentialTarget.Ceiling }, browsed)
|
||||
.Where(t => !t.IsArray && t.Type is { } type && Atomic.Contains(type))
|
||||
.DistinctBy(t => t.Name)
|
||||
.Take(target.LatencyTags)
|
||||
.ToList();
|
||||
if (candidates.Count == 0 || candidates.Count >= target.LatencyTags) return (candidates, candidates.Count, false);
|
||||
var repeated = Enumerable.Range(0, target.LatencyTags).Select(i => candidates[i % candidates.Count]).ToList();
|
||||
return (repeated, candidates.Count, true);
|
||||
}
|
||||
|
||||
public static async Task<LatencyResult> ExecuteAsync(DifferentialTarget target, DifferentialResult differential, CancellationToken ct)
|
||||
{
|
||||
var (picked, distinct, repeated) = PickTags(target, differential.Libplctag);
|
||||
if (picked.Count == 0)
|
||||
throw new InvalidOperationException("The browse offered no atomic scalar tags to time; nothing was read.");
|
||||
|
||||
// The differential's native session must be gone before libplctag connects.
|
||||
await DifferentialRun.WaitForSessionsToCloseAsync(
|
||||
target, differential.Addresses, differential.BaselineEndpoints, "the differential's native", ct).ConfigureAwait(false);
|
||||
|
||||
// ---- libplctag: setup, passes, every handle disposed (anchor last). ----
|
||||
var dropped = new List<string>();
|
||||
var libFactory = AbCipTransportFactories.For(AbCipTransport.Libplctag).Tags;
|
||||
// Anchor on a tag the differential already read Ok through libplctag, so the anchor is never the refusal.
|
||||
var anchorTag = picked.FirstOrDefault(t => differential.Libplctag.Reads.TryGetValue(t.Name, out var o) && o.Succeeded)
|
||||
?? picked[0];
|
||||
var (libPasses, tags) = await DifferentialRun.WithLibplctagAnchorAsync("latency libplctag", target, anchorTag.Name, async () =>
|
||||
{
|
||||
var runtimes = new List<(SelectedTag Tag, IAbCipTagRuntime Runtime)>();
|
||||
try
|
||||
{
|
||||
foreach (var tag in picked)
|
||||
{
|
||||
var rt = await CreateAsync("latency libplctag", target, libFactory, tag, ct).ConfigureAwait(false);
|
||||
if (rt is null) { dropped.Add(tag.Name); continue; }
|
||||
runtimes.Add((tag, rt));
|
||||
}
|
||||
var passes = await RunPassesAsync("latency libplctag", target, runtimes, countRequests: false, afterEach: null, ct)
|
||||
.ConfigureAwait(false);
|
||||
return (passes, (IReadOnlyList<SelectedTag>)runtimes.Select(r => r.Tag).ToList());
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var (_, rt) in runtimes) rt.Dispose();
|
||||
}
|
||||
}, ct).ConfigureAwait(false);
|
||||
|
||||
// ---- Barrier: libplctag's session must be gone before the native side connects. ----
|
||||
await DifferentialRun.WaitForSessionsToCloseAsync(
|
||||
target, differential.Addresses, differential.BaselineEndpoints, "libplctag", ct).ConfigureAwait(false);
|
||||
|
||||
// ---- native: the same (post-drop) set, under one anchor lease. ----
|
||||
var problems = new List<string>();
|
||||
var nativeFactory = AbCipTransportFactories.For(AbCipTransport.Native).Tags;
|
||||
var ((nativePasses, setupRequests), opens, closes) = await DifferentialRun.WithNativeAnchorAsync("latency native", target, async checkOpens =>
|
||||
{
|
||||
var runtimes = new List<(SelectedTag Tag, IAbCipTagRuntime Runtime)>();
|
||||
try
|
||||
{
|
||||
var before = Volatile.Read(ref CipConnection.RequestsSent);
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
var rt = await CreateAsync("latency native", target, nativeFactory, tag, ct).ConfigureAwait(false);
|
||||
checkOpens($"setup of '{tag.Name}'");
|
||||
if (rt is null) { problems.Add($"native could not read '{tag.Name}', which libplctag read"); continue; }
|
||||
runtimes.Add((tag, rt));
|
||||
}
|
||||
var setup = Volatile.Read(ref CipConnection.RequestsSent) - before;
|
||||
var passes = await RunPassesAsync("latency native", target, runtimes, countRequests: true, checkOpens, ct)
|
||||
.ConfigureAwait(false);
|
||||
return (passes, setup);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var (_, rt) in runtimes) rt.Dispose();
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
// ---- Barrier: the native session must be gone before the run reports (review r1 M9). ----
|
||||
var afterNative = await DifferentialRun.WaitForSessionsToCloseAsync(
|
||||
target, differential.Addresses, differential.BaselineEndpoints, "latency native", ct).ConfigureAwait(false);
|
||||
|
||||
return new LatencyResult
|
||||
{
|
||||
Target = target,
|
||||
Tags = tags,
|
||||
DistinctTags = distinct,
|
||||
Repeated = repeated,
|
||||
Dropped = dropped,
|
||||
Libplctag = libPasses,
|
||||
Native = nativePasses,
|
||||
NativeForwardOpens = opens,
|
||||
NativeForwardCloses = closes,
|
||||
ConnectionsAfterNative = afterNative,
|
||||
NativeSetupRequests = setupRequests,
|
||||
Problems = problems,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Create + first read of one tag. Null on a tag-level refusal; a connection-level failure aborts.</summary>
|
||||
private static async Task<IAbCipTagRuntime?> CreateAsync(
|
||||
string phase, DifferentialTarget target, IAbCipTagFactory factory, SelectedTag tag, CancellationToken ct)
|
||||
{
|
||||
var rt = factory.Create(target.Params(tag.Name, tag.ElementCount, tag.IsArray, tag.Type));
|
||||
try
|
||||
{
|
||||
await DifferentialRun.PaceAsync(target, ct).ConfigureAwait(false);
|
||||
await rt.InitializeAsync(ct).ConfigureAwait(false);
|
||||
if (rt.GetStatus() == (int)AbStatus.Ok) return rt;
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException || !ct.IsCancellationRequested)
|
||||
{
|
||||
if (DifferentialRun.IsConnectionLevel(ex))
|
||||
{
|
||||
rt.Dispose();
|
||||
throw DifferentialRun.Abort(phase, $"setup of '{tag.Name}'", ex);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
rt.Dispose();
|
||||
throw;
|
||||
}
|
||||
rt.Dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
private static async Task<List<LatencyPass>> RunPassesAsync(
|
||||
string phase, DifferentialTarget target, List<(SelectedTag Tag, IAbCipTagRuntime Runtime)> runtimes,
|
||||
bool countRequests, Action<string>? afterEach, CancellationToken ct)
|
||||
{
|
||||
var passes = new List<LatencyPass>(target.LatencyPasses);
|
||||
for (var p = 1; p <= target.LatencyPasses; p++)
|
||||
{
|
||||
if (p > 1 && target.LatencyPassGapMs > 0) await Task.Delay(target.LatencyPassGapMs, ct).ConfigureAwait(false);
|
||||
|
||||
var requestsBefore = Volatile.Read(ref CipConnection.RequestsSent);
|
||||
CipConnection.ResetExchangeStats();
|
||||
var gcBefore = GcDelta.Now();
|
||||
var clock = Stopwatch.StartNew();
|
||||
var outcomes = await Task.WhenAll(runtimes.Select(r => ReadOnceAsync(r.Runtime, ct))).ConfigureAwait(false);
|
||||
clock.Stop();
|
||||
var gc = GcDelta.Now() - gcBefore;
|
||||
var rtt = countRequests ? CipConnection.ReadExchangeStats() : (CipConnection.ExchangeStats?)null;
|
||||
var requests = Volatile.Read(ref CipConnection.RequestsSent) - requestsBefore;
|
||||
|
||||
var failures = 0;
|
||||
for (var i = 0; i < outcomes.Length; i++)
|
||||
{
|
||||
if (outcomes[i] is { } ex)
|
||||
{
|
||||
if (DifferentialRun.IsConnectionLevel(ex))
|
||||
throw DifferentialRun.Abort(phase, $"pass {p}, tag '{runtimes[i].Tag.Name}'", ex);
|
||||
failures++;
|
||||
}
|
||||
else if (runtimes[i].Runtime.GetStatus() != (int)AbStatus.Ok)
|
||||
{
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
afterEach?.Invoke($"pass {p}");
|
||||
passes.Add(new LatencyPass(p, clock.Elapsed, countRequests ? requests : null, failures, gc, rtt));
|
||||
}
|
||||
return passes;
|
||||
}
|
||||
|
||||
private static async Task<Exception?> ReadOnceAsync(IAbCipTagRuntime runtime, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
await runtime.ReadAsync(ct).ConfigureAwait(false);
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException || !ct.IsCancellationRequested)
|
||||
{
|
||||
return ex;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Nearest-rank percentile (p in (0,100]) of the pass wall times, in milliseconds.</summary>
|
||||
internal static double Percentile(IEnumerable<TimeSpan> walls, double p)
|
||||
{
|
||||
var sorted = walls.Select(w => w.TotalMilliseconds).Order().ToList();
|
||||
if (sorted.Count == 0) throw new InvalidOperationException("No passes to take a percentile of.");
|
||||
var rank = (int)Math.Ceiling(p / 100.0 * sorted.Count);
|
||||
return sorted[Math.Clamp(rank, 1, sorted.Count) - 1];
|
||||
}
|
||||
}
|
||||
+165
-14
@@ -95,25 +95,37 @@ public sealed class TransportDifferentialTests(DifferentialRunFixture fixture)
|
||||
Log($"## Reads — {r.Selection.Count} tag(s), {r.Target}");
|
||||
Log("Selection by origin: " + string.Join(", ",
|
||||
r.Selection.GroupBy(t => t.Origin).Select(g => $"{g.Key} {g.Count()}")));
|
||||
Log("| Tag | Type | libplctag status | native status | Equal? |");
|
||||
Log("|---|---|---|---|---|");
|
||||
Log("| Tag | Type | libplctag status | native status | libplctag probe | Equal? |");
|
||||
Log("|---|---|---|---|---|---|");
|
||||
|
||||
var unexpected = new List<string>();
|
||||
var expected = new List<string>();
|
||||
var live = new List<string>();
|
||||
foreach (var tag in r.Selection)
|
||||
{
|
||||
var lib = r.Libplctag.Reads[tag.Name];
|
||||
var nat = r.Native.Reads[tag.Name];
|
||||
var verdict = Compare(tag, lib, nat);
|
||||
if (verdict.StartsWith("expected", StringComparison.Ordinal)) expected.Add($"{tag.Name}: {verdict}");
|
||||
else if (verdict != "yes") unexpected.Add($"{tag.Name}: {verdict} (lib {Detail(lib)}; native {Detail(nat)})");
|
||||
var probe = r.Libplctag.ProbeReads.GetValueOrDefault(tag.Name);
|
||||
var verdict = Compare(tag, lib, nat, probe);
|
||||
switch (Classify(verdict))
|
||||
{
|
||||
case Verdict.Expected: expected.Add($"{tag.Name}: {verdict}"); break;
|
||||
case Verdict.Live: live.Add($"{tag.Name} (lib {Detail(lib, full: true)}; lib probe {Detail(probe!, full: true)}; native {Detail(nat, full: true)})"); break;
|
||||
case Verdict.Unexpected when DifferentialRun.IsLive(lib, probe):
|
||||
unexpected.Add($"{tag.Name}: {verdict} (live; lib {Detail(lib, full: true)}; lib probe {Detail(probe!, full: true)}; native {Detail(nat, full: true)})");
|
||||
break;
|
||||
case Verdict.Unexpected: unexpected.Add($"{tag.Name}: {verdict} (lib {Detail(lib)}; native {Detail(nat)})"); break;
|
||||
}
|
||||
Log($"| `{tag.Name}` | {tag.Type?.ToString() ?? "?"}{(tag.IsArray ? $"[{tag.ElementCount}]" : "")} | " +
|
||||
$"{StatusName(lib.Status)} | {StatusName(nat.Status)} | {verdict} |");
|
||||
$"{StatusName(lib.Status)} | {StatusName(nat.Status)} | {ProbeColumn(lib, probe)} | {verdict} |");
|
||||
}
|
||||
|
||||
Log("");
|
||||
Log($"Unexpected divergences: {unexpected.Count}; expected divergences: {expected.Count}.");
|
||||
Log($"Unexpected divergences: {unexpected.Count}; expected divergences: {expected.Count}; " +
|
||||
$"live tags (value moved between the two libplctag reads; compared by status, length, type code, shape and every " +
|
||||
$"element that held still): {live.Count} of {r.Selection.Count}.");
|
||||
foreach (var e in expected) Log($"- expected: {e}");
|
||||
foreach (var l in live) Log($"- live: {l}");
|
||||
foreach (var u in unexpected) Log($"- UNEXPECTED: {u}");
|
||||
|
||||
var coverage = DifferentialRun.SelectionCoverageProblems(r.Target, r.Libplctag, r.Selection);
|
||||
@@ -138,6 +150,21 @@ public sealed class TransportDifferentialTests(DifferentialRunFixture fixture)
|
||||
r.ConnectionsBeforeNative.Value.ShouldBeLessThanOrEqualTo(r.ConnectionsBaseline.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// T21 press pass 1 ended in "A callback was made on a garbage collected delegate": libplctag tags whose
|
||||
/// initialisation failed (NotFound tags, refused latency setups) were left native-side with a live callback.
|
||||
/// Every libplctag tag this harness creates goes through the driver's adapters, which now destroy such a tag
|
||||
/// (<see cref="LibplctagOrphanRelease"/>); this pins that none had to fall back to being rooted instead.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task No_failed_libplctag_tag_is_left_with_a_live_native_callback()
|
||||
{
|
||||
await RunOrSkipAsync();
|
||||
Log($"libplctag orphans destroyed so far: {LibplctagOrphanRelease.Released}; rooted (unreleasable): {LibplctagOrphanRelease.RootedCount}.");
|
||||
LibplctagOrphanRelease.CanReadHandles.ShouldBeTrue();
|
||||
LibplctagOrphanRelease.RootedCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_native_side_makes_exactly_one_forward_open_over_the_whole_run()
|
||||
{
|
||||
@@ -149,6 +176,25 @@ public sealed class TransportDifferentialTests(DifferentialRunFixture fixture)
|
||||
r.NativeForwardOpens.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// T21 review r1 M9: the native session is closed properly — exactly one Forward Close on the wire (a skipped
|
||||
/// close, e.g. on a socket already dropped, would otherwise be visible only in a capture), and no connection to
|
||||
/// the target left once the anchor is released.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task The_native_side_closes_its_one_connection_with_exactly_one_forward_close()
|
||||
{
|
||||
var r = await RunOrSkipAsync();
|
||||
Log($"Native Forward Closes sent/acked: {r.NativeForwardCloses.Sent}/{r.NativeForwardCloses.Acked}; " +
|
||||
$"connections after the native phase: {r.ConnectionsAfterNative?.ToString(CultureInfo.InvariantCulture) ?? "unobservable"}.");
|
||||
if (r.ConnectionsAfterNative is { } after && r.ConnectionsBaseline is { } baseline)
|
||||
after.ShouldBeLessThanOrEqualTo(baseline);
|
||||
if (!r.Target.CountOpens)
|
||||
Assert.Skip($"{DifferentialTarget.CountOpensVar} is not 1 — the process-wide Forward Close counter is only " +
|
||||
"trusted when the differential runs alone.");
|
||||
r.NativeForwardCloses.Sent.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Offline guard — deliberately NOT gated on the endpoint, so it runs in every build. Fails if any
|
||||
/// source file of this harness names a write-capable member, and, independently of the source text,
|
||||
@@ -169,6 +215,10 @@ public sealed class TransportDifferentialTests(DifferentialRunFixture fixture)
|
||||
var dir = Path.GetDirectoryName(ThisFile())!;
|
||||
var sources = Directory.GetFiles(dir, "*.cs");
|
||||
sources.ShouldContain(f => Path.GetFileName(f) == nameof(TransportDifferentialTests) + ".cs");
|
||||
// Task 21's latency comparison and its run helper are part of the harness the press sees: pinned in scope,
|
||||
// so moving either out of this folder (or namespace) fails the guard instead of silently escaping it.
|
||||
sources.ShouldContain(f => Path.GetFileName(f) == nameof(PollLatencyComparisonTests) + ".cs");
|
||||
sources.ShouldContain(f => Path.GetFileName(f) == nameof(PollLatencyRun) + ".cs");
|
||||
var textHits = (from f in sources
|
||||
let text = File.ReadAllText(f)
|
||||
from token in forbidden
|
||||
@@ -179,6 +229,8 @@ public sealed class TransportDifferentialTests(DifferentialRunFixture fixture)
|
||||
var harnessTypes = typeof(TransportDifferentialTests).Assembly.GetTypes()
|
||||
.Where(t => t.Namespace == typeof(TransportDifferentialTests).Namespace).ToList();
|
||||
harnessTypes.ShouldContain(typeof(TransportDifferentialTests));
|
||||
harnessTypes.ShouldContain(typeof(PollLatencyComparisonTests));
|
||||
harnessTypes.ShouldContain(typeof(PollLatencyRun));
|
||||
var callHits = (from t in harnessTypes
|
||||
from m in MethodsOf(t)
|
||||
from callee in CalleesOf(m)
|
||||
@@ -217,13 +269,89 @@ public sealed class TransportDifferentialTests(DifferentialRunFixture fixture)
|
||||
Compare(array, arr, arr with { Decoded = new[] { float.NaN, 2f } }).ShouldBe("**NO: value**");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Offline pin of the live-drift handling (T21 press pass 1): a value that moved between the two libplctag
|
||||
/// reads is compared structurally, while a value libplctag read stably and native read differently is still
|
||||
/// an unexpected divergence. Removing the live handling turns the first assertion into "**NO: raw**".
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void A_live_value_is_compared_structurally_and_a_stable_one_by_value()
|
||||
{
|
||||
var real = new SelectedTag("WW_M4ActualTemp", AbCipDataType.Real, 1, false, "scalar");
|
||||
ReadObservation Obs(float v) => new((int)AbStatus.Ok, null, BitConverter.GetBytes(v), v);
|
||||
var lib = Obs(384.88684f);
|
||||
var probe = Obs(384.95f);
|
||||
var nat = Obs(385.13782f) with { TypeCode = 0xCA };
|
||||
|
||||
// Live: the libplctag value moved by itself, so a different native value is not a divergence.
|
||||
DifferentialRun.IsLive(lib, probe).ShouldBeTrue();
|
||||
Compare(real, lib, nat, probe).ShouldBe("live");
|
||||
Classify(Compare(real, lib, nat, probe)).ShouldBe(Verdict.Live);
|
||||
|
||||
// ...but everything structural still has to match.
|
||||
Compare(real, lib, nat with { Status = (int)AbStatus.NotFound, Error = "NotFound" }, probe).ShouldBe("**NO: status**");
|
||||
Compare(real, lib, nat with { Raw = new byte[8] }, probe).ShouldBe("**NO: raw length**");
|
||||
Compare(real, lib, nat with { Raw = null }, probe).ShouldBe("**NO: raw length**");
|
||||
Compare(real, lib with { TypeCode = 0xC4 }, nat, probe).ShouldBe("**NO: type code**");
|
||||
Compare(real, lib, nat with { Decoded = 385 }, probe).ShouldBe("**NO: shape**");
|
||||
Compare(real, lib, nat with { Decoded = new[] { 385f } }, probe).ShouldBe("**NO: shape**");
|
||||
Compare(real, lib, nat with { DecodeError = "boom" }, probe).ShouldBe("**NO: decode**");
|
||||
Classify("**NO: raw length**").ShouldBe(Verdict.Unexpected);
|
||||
|
||||
var array = new SelectedTag("A", AbCipDataType.Real, 2, true, "array");
|
||||
var arrLib = new ReadObservation((int)AbStatus.Ok, null, new byte[8], new[] { 1f, 2f });
|
||||
var arrProbe = arrLib with { Raw = [1, 0, 0, 0, 0, 0, 0, 0], Decoded = new[] { 1.1f, 2f } };
|
||||
// Element 0 moved (1 → 1.1), element 1 held at 2: native may read anything finite for 0, but must read 2 for 1.
|
||||
Compare(array, arrLib, arrLib with { Raw = [2, 0, 0, 0, 0, 0, 0, 0], Decoded = new[] { 3f, 2f } }, arrProbe).ShouldBe("live");
|
||||
Compare(array, arrLib, arrLib with { Raw = [2, 0, 0, 0, 0, 0, 0, 0], Decoded = new[] { 3f, 4f } }, arrProbe)
|
||||
.ShouldBe("**NO: stable element 1**");
|
||||
Compare(array, arrLib, arrLib with { Decoded = new[] { 1f, 2f, 3f } }, arrProbe).ShouldBe("**NO: shape**");
|
||||
|
||||
// Review r1 I1, the pass-1 shape: Real[5] where element 0 (400000) holds still and 1..4 move. A native defect
|
||||
// that shifts the stable element must FAIL even though the tag is live.
|
||||
var fout = new SelectedTag("Program:EM01_PowerUnit.__fout9FB1561FA00D73F5", AbCipDataType.Real, 5, true, "udt member");
|
||||
ReadObservation Arr(params float[] v) => new((int)AbStatus.Ok, null, v.SelectMany(BitConverter.GetBytes).ToArray(), v);
|
||||
var foutLib = Arr(400000f, 4.2464733f, 5.2464733f, -6.7535267f, 4.2464733f);
|
||||
var foutProbe = Arr(400000f, 4.2461f, 5.2461f, -6.7539f, 4.2461f);
|
||||
Compare(fout, foutLib, Arr(400000f, 4.2457356f, 5.2457356f, -6.7542644f, 4.2457356f), foutProbe).ShouldBe("live");
|
||||
var shifted = Compare(fout, foutLib, Arr(4.2457356f, 5.2457356f, -6.7542644f, 4.2457356f, 400000f), foutProbe);
|
||||
shifted.ShouldBe("**NO: stable element 0**");
|
||||
Classify(shifted).ShouldBe(Verdict.Unexpected);
|
||||
Compare(fout, foutLib, Arr(400000f, float.NaN, 5.2457356f, -6.7542644f, 4.2457356f), foutProbe)
|
||||
.ShouldBe("**NO: non-finite element 1**");
|
||||
Compare(real, lib, nat with { Decoded = float.PositiveInfinity }, probe).ShouldBe("**NO: non-finite element 0**");
|
||||
|
||||
// Stable: the two libplctag reads agree, so native reading anything else is a real decoding divergence.
|
||||
var stableProbe = lib with { };
|
||||
DifferentialRun.IsLive(lib, stableProbe).ShouldBeFalse();
|
||||
Compare(real, lib, nat, stableProbe).ShouldBe("**NO: raw**");
|
||||
Classify(Compare(real, lib, nat, stableProbe)).ShouldBe(Verdict.Unexpected);
|
||||
Compare(real, lib, lib with { Decoded = 1f }, stableProbe).ShouldBe("**NO: value**");
|
||||
Compare(real, lib, lib with { }, stableProbe).ShouldBe("yes");
|
||||
Classify("yes").ShouldBe(Verdict.Equal);
|
||||
|
||||
// A missing or failed probe is never live: the full value comparison applies.
|
||||
Compare(real, lib, nat, null).ShouldBe("**NO: raw**");
|
||||
Compare(real, lib, nat, new ReadObservation((int)AbStatus.Timeout, "Timeout", null, null)).ShouldBe("**NO: raw**");
|
||||
}
|
||||
|
||||
private async Task<DifferentialResult> RunOrSkipAsync()
|
||||
{
|
||||
if (DifferentialTarget.FromEnvironment() is null) Assert.Skip(DifferentialTarget.SkipReason);
|
||||
return await fixture.GetAsync();
|
||||
}
|
||||
|
||||
internal static string Compare(SelectedTag tag, ReadObservation lib, ReadObservation nat)
|
||||
internal enum Verdict { Equal, Expected, Live, Unexpected }
|
||||
|
||||
/// <summary>Which list a verdict string belongs to. Live tags are neither expected nor unexpected divergences.</summary>
|
||||
internal static Verdict Classify(string verdict) =>
|
||||
verdict == "yes" ? Verdict.Equal
|
||||
: verdict == "live" ? Verdict.Live
|
||||
: verdict.StartsWith("expected", StringComparison.Ordinal) ? Verdict.Expected
|
||||
: Verdict.Unexpected;
|
||||
|
||||
/// <param name="libProbe">libplctag's second (volatility-probe) read of the tag; null ⇒ treated as stable.</param>
|
||||
internal static string Compare(SelectedTag tag, ReadObservation lib, ReadObservation nat, ReadObservation? libProbe = null)
|
||||
{
|
||||
// Spec §5: native refuses a returned type that disagrees with the authored one; libplctag decodes it anyway.
|
||||
if (nat.Status == (int)AbStatus.TypeMismatch && lib.Status == (int)AbStatus.Ok)
|
||||
@@ -232,21 +360,44 @@ public sealed class TransportDifferentialTests(DifferentialRunFixture fixture)
|
||||
if (!lib.Succeeded || !nat.Succeeded)
|
||||
return lib.Succeeded == nat.Succeeded ? "yes" : "**NO: error**";
|
||||
if (lib.DecodeError is not null || nat.DecodeError is not null) return "**NO: decode**";
|
||||
if (DifferentialRun.IsLive(lib, libProbe))
|
||||
{
|
||||
// The value moved between libplctag's own two reads, so native reading a third value proves nothing.
|
||||
// Everything that does not depend on the value must still agree.
|
||||
if (nat.Raw is null || nat.Raw.Length != lib.Raw!.Length) return "**NO: raw length**";
|
||||
if (lib.TypeCode is { } lt && nat.TypeCode is { } nt && lt != nt) return "**NO: type code**";
|
||||
if (tag.Type is null) return "live"; // raw-only (type unknown): nothing decoded to compare element-wise
|
||||
if (!DifferentialRun.SameShape(lib.Decoded, nat.Decoded)) return "**NO: shape**";
|
||||
// Only what MOVED between libplctag's two reads is excused (review r1 I1): every element that held still
|
||||
// must read the same through native, so a native offset/stride/byte-order defect cannot hide as "live".
|
||||
return DifferentialRun.LiveElementMismatch(lib.Decoded, libProbe!.Decoded, nat.Decoded) ?? "live";
|
||||
}
|
||||
if (lib.Raw is null || nat.Raw is null || !lib.Raw.AsSpan().SequenceEqual(nat.Raw)) return "**NO: raw**";
|
||||
if (tag.Type is not null && !DifferentialRun.ValuesEqual(lib.Decoded, nat.Decoded)) return "**NO: value**";
|
||||
return "yes";
|
||||
}
|
||||
|
||||
private static string Detail(ReadObservation o) =>
|
||||
o.Error ?? o.DecodeError ?? $"{o.Raw?.Length.ToString(CultureInfo.InvariantCulture) ?? "-"} byte(s) {Hex(o.Raw)} = {Show(o.Decoded)}";
|
||||
/// <param name="full">Every raw byte and every element (live tags: the evidence must be re-checkable by hand).</param>
|
||||
private static string Detail(ReadObservation o, bool full = false) =>
|
||||
o.Error ?? o.DecodeError ??
|
||||
$"{o.Raw?.Length.ToString(CultureInfo.InvariantCulture) ?? "-"} byte(s) {Hex(o.Raw, full)} = {Show(o.Decoded, full)}";
|
||||
|
||||
private static string Hex(byte[]? raw) =>
|
||||
raw is null ? "-" : Convert.ToHexString(raw.AsSpan(0, Math.Min(raw.Length, 32))) + (raw.Length > 32 ? "…" : "");
|
||||
private static string ProbeColumn(ReadObservation lib, ReadObservation? probe) =>
|
||||
probe is null ? "-"
|
||||
: DifferentialRun.IsLive(lib, probe) ? "**live**"
|
||||
: probe.Succeeded && lib.Succeeded ? "stable"
|
||||
: StatusName(probe.Status);
|
||||
|
||||
private static string Show(object? v) => v switch
|
||||
private static string Hex(byte[]? raw, bool full = false) =>
|
||||
raw is null ? "-"
|
||||
: full || raw.Length <= 32 ? Convert.ToHexString(raw)
|
||||
: Convert.ToHexString(raw.AsSpan(0, 32)) + "…";
|
||||
|
||||
private static string Show(object? v, bool full = false) => v switch
|
||||
{
|
||||
null => "null",
|
||||
Array a => "[" + string.Join(",", a.Cast<object?>().Take(8).Select(Show)) + (a.Length > 8 ? ",…" : "") + "]",
|
||||
Array a => "[" + string.Join(",", a.Cast<object?>().Take(full ? int.MaxValue : 8).Select(x => Show(x))) +
|
||||
(!full && a.Length > 8 ? ",…" : "") + "]",
|
||||
IFormattable f => f.ToString(null, CultureInfo.InvariantCulture),
|
||||
_ => v.ToString() ?? "",
|
||||
};
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
using libplctag.NativeImport;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// T21 press pass 1 crash, the variant that killed the test host: a tag the controller does not have. libplctag
|
||||
/// raises <c>Created</c> with NotFound, so libplctag.NET 1.5.2 throws WITHOUT unregistering its callback, and its
|
||||
/// <c>Dispose()</c> skips the uninitialised tag. Before the fix the native tag survived with a live callback into a
|
||||
/// delegate the GC was free to collect; libplctag's <c>atexit</c> shutdown then raised DESTROYED into it ("A callback
|
||||
/// was made on a garbage collected delegate"). This pins that the native tag is destroyed instead.
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Requires", "AbServer")]
|
||||
public sealed class LibplctagOrphanReleaseLiveTests
|
||||
{
|
||||
[AbServerFact]
|
||||
public async Task A_tag_the_controller_lacks_leaves_no_native_tag_behind()
|
||||
{
|
||||
var fixture = new AbServerFixture();
|
||||
var rt = new LibplctagTagRuntime(new AbCipTagCreateParams(
|
||||
Gateway: fixture.Host, Port: fixture.Port, CipPath: "1,0", LibplctagPlcAttribute: "controllogix",
|
||||
TagName: "T21_NoSuchTag", Timeout: TimeSpan.FromSeconds(5)));
|
||||
try
|
||||
{
|
||||
var ex = await Should.ThrowAsync<AbTransportException>(() => rt.InitializeAsync(CancellationToken.None));
|
||||
ex.Status.ShouldBe(AbStatus.NotFound);
|
||||
rt.GetStatus().ShouldBe((int)AbStatus.NotFound);
|
||||
|
||||
var handle = rt.NativeHandle;
|
||||
handle.ShouldBeGreaterThan(0);
|
||||
var exists = plctag.plc_tag_lock(handle) == 0;
|
||||
if (exists) plctag.plc_tag_unlock(handle);
|
||||
exists.ShouldBeFalse("a NotFound tag must not survive with its callback registered");
|
||||
}
|
||||
finally
|
||||
{
|
||||
rt.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,4 +202,26 @@ public sealed class AbCipDriverProbeTests
|
||||
result.Ok.ShouldBeTrue();
|
||||
result.Message!.ShouldContain("probe tag not found");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// T21: the probe builds its handle from the family profile, so a CompactLogix device with no
|
||||
/// ConnectionSize opens Large (4002) like libplctag; an explicit device ConnectionSize still wins.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("", 4002)]
|
||||
[InlineData(",\"connectionSize\":504", 504)]
|
||||
public async Task CompactLogix_probe_uses_the_profile_default_unless_the_device_overrides_it(string extra, int expected)
|
||||
{
|
||||
using var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
var factory = new FakeAbCipTagFactory();
|
||||
|
||||
await new AbCipDriverProbe(factory).ProbeAsync(
|
||||
$"{{\"devices\":[{{\"hostAddress\":\"ab://127.0.0.1:{port}/1,0\",\"plcFamily\":\"CompactLogix\"{extra}}}]}}",
|
||||
TimeSpan.FromSeconds(3),
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
factory.Tags.Values.ShouldHaveSingleItem().CreationParams.ConnectionSize.ShouldBe(expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ public sealed class AbCipDriverTests
|
||||
AbCipPlcFamilyProfile.Micro800.SupportsRequestPacking.ShouldBeFalse();
|
||||
AbCipPlcFamilyProfile.Micro800.SupportsConnectedMessaging.ShouldBeFalse();
|
||||
|
||||
AbCipPlcFamilyProfile.CompactLogix.DefaultConnectionSize.ShouldBe(504);
|
||||
AbCipPlcFamilyProfile.CompactLogix.DefaultConnectionSize.ShouldBe(4002); // Large, as libplctag opens it
|
||||
AbCipPlcFamilyProfile.GuardLogix.LibplctagPlcAttribute.ShouldBe("controllogix");
|
||||
}
|
||||
|
||||
|
||||
+23
@@ -117,6 +117,29 @@ public sealed class AbCipPerDeviceConnectionOptionsTests
|
||||
factory.Tags["Speed"].CreationParams.ConnectionSize.ShouldBe(4002);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// T21: a CompactLogix device with no override opens Large (4002), matching libplctag's
|
||||
/// <c>plc=compactlogix</c>; the per-device override still wins.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(null, 4002)]
|
||||
[InlineData(504, 504)]
|
||||
public async Task CompactLogix_ConnectionSize_defaults_to_large_unless_overridden(int? overrideSize, int expected)
|
||||
{
|
||||
var factory = new FakeAbCipTagFactory();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions(Device, AbCipPlcFamily.CompactLogix, ConnectionSize: overrideSize)],
|
||||
RawTags = AbCipRawTags.From(new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt)),
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
}, "drv-1", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||
|
||||
factory.Tags["Speed"].CreationParams.ConnectionSize.ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that AllowPacking and ConnectionSize round-trip correctly through JSON parsing.</summary>
|
||||
[Fact]
|
||||
public void AbCipDriverFactoryExtensions_ParseOptions_round_trips_AllowPacking_and_ConnectionSize()
|
||||
|
||||
@@ -40,22 +40,26 @@ public sealed class AbCipPlcFamilyTests
|
||||
|
||||
// ---- CompactLogix ----
|
||||
|
||||
/// <summary>Verifies that the CompactLogix profile uses a narrower connection size than ControlLogix.</summary>
|
||||
/// <summary>
|
||||
/// Verifies that the CompactLogix profile defaults to a Large Forward Open (4002), matching what
|
||||
/// libplctag opens for <c>plc=compactlogix</c>. A target that refuses Large falls back to a regular
|
||||
/// 504 open inside CipConnection (pinned by the Ab.Transport CipConnectionTests fallback tests).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CompactLogix_profile_uses_narrower_connection_size()
|
||||
public void CompactLogix_profile_defaults_to_a_large_forward_open_like_libplctag()
|
||||
{
|
||||
var p = AbCipPlcFamilyProfile.CompactLogix;
|
||||
p.LibplctagPlcAttribute.ShouldBe("compactlogix");
|
||||
p.DefaultConnectionSize.ShouldBe(504); // 5069-L3x narrow-window safety
|
||||
p.DefaultConnectionSize.ShouldBe(4002); // T21: 504 cost the native transport ~3x the round trips per poll
|
||||
p.DefaultCipPath.ShouldBe("1,0");
|
||||
p.SupportsRequestPacking.ShouldBeTrue();
|
||||
p.SupportsConnectedMessaging.ShouldBeTrue();
|
||||
p.MaxFragmentBytes.ShouldBe(500);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a CompactLogix device initializes with a narrow connection size.</summary>
|
||||
/// <summary>Verifies that a CompactLogix device initializes with the Large connection size libplctag opens (T21).</summary>
|
||||
[Fact]
|
||||
public async Task CompactLogix_device_initialises_with_narrow_ConnectionSize()
|
||||
public async Task CompactLogix_device_initialises_with_large_ConnectionSize()
|
||||
{
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
@@ -65,7 +69,7 @@ public sealed class AbCipPlcFamilyTests
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
var profile = drv.GetDeviceState("ab://192.168.1.10/1,0")!.Profile;
|
||||
profile.DefaultConnectionSize.ShouldBeLessThan(AbCipPlcFamilyProfile.ControlLogix.DefaultConnectionSize);
|
||||
profile.DefaultConnectionSize.ShouldBe(4002);
|
||||
profile.MaxFragmentBytes.ShouldBeLessThan(AbCipPlcFamilyProfile.ControlLogix.MaxFragmentBytes);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
using libplctag.NativeImport;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// T21 press pass 1: a libplctag tag whose initialisation FAILED used to leave its native tag behind, because
|
||||
/// libplctag.NET 1.5.2's <c>Dispose()</c> only destroys an initialised tag. Its callback delegate then died with
|
||||
/// the wrapper, and libplctag's <c>atexit</c> shutdown raised DESTROYED into it: "A callback was made on a garbage
|
||||
/// collected delegate". These pin that a failed initialisation destroys the native tag before it propagates.
|
||||
/// The NotFound variant (the one that crashed, callback still registered) needs a controller and lives in
|
||||
/// <c>AbCip.IntegrationTests/LibplctagOrphanReleaseLiveTests</c>; this offline one is the refused-connection
|
||||
/// variant, whose native tag otherwise keeps reconnecting in the background for the life of the process.
|
||||
/// </summary>
|
||||
[CollectionDefinition(Name, DisableParallelization = true)]
|
||||
public sealed class LibplctagOrphanReleaseCollection
|
||||
{
|
||||
/// <summary>The release counters and the rooting seam are process-wide: nothing else may run alongside.</summary>
|
||||
public const string Name = "libplctag orphan release";
|
||||
}
|
||||
|
||||
[Collection(LibplctagOrphanReleaseCollection.Name)]
|
||||
public sealed class LibplctagOrphanReleaseTests
|
||||
{
|
||||
private static AbCipTagCreateParams Refused(string tagName = "TestDINT") => new(
|
||||
Gateway: "127.0.0.1", Port: 1, CipPath: "1,0", LibplctagPlcAttribute: "controllogix",
|
||||
TagName: tagName, Timeout: TimeSpan.FromMilliseconds(300));
|
||||
|
||||
[Fact]
|
||||
public void The_pinned_libplctag_wrapper_still_exposes_the_native_handle()
|
||||
{
|
||||
// A libplctag bump that renames Tag._tag / NativeTagWrapper.nativeTagHandle disables the release (the tag is
|
||||
// then rooted for the process lifetime instead). Fail here so the bump is reviewed against the release.
|
||||
LibplctagOrphanRelease.CanReadHandles.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_failed_initialisation_destroys_the_native_tag_it_created()
|
||||
{
|
||||
// Port 1 on loopback refuses the connection; libplctag keeps retrying until the timeout.
|
||||
var rt = new LibplctagTagRuntime(new AbCipTagCreateParams(
|
||||
Gateway: "127.0.0.1", Port: 1, CipPath: "1,0", LibplctagPlcAttribute: "controllogix",
|
||||
TagName: "TestDINT", Timeout: TimeSpan.FromMilliseconds(300)));
|
||||
try
|
||||
{
|
||||
await Should.ThrowAsync<AbTransportException>(() => rt.InitializeAsync(CancellationToken.None));
|
||||
var handle = rt.NativeHandle;
|
||||
handle.ShouldBeGreaterThan(0, "plc_tag_create_ex returned a handle before the failure");
|
||||
NativeTagExists(handle).ShouldBeFalse("the failed tag's native handle must be destroyed, not orphaned");
|
||||
// The status the tag held is kept, not replaced by the destroyed handle's NotFound.
|
||||
rt.GetStatus().ShouldNotBe((int)AbStatus.NotFound);
|
||||
// Review r1 M2: the wrapper no longer holds the destroyed id, so a repeat release cannot touch it.
|
||||
rt.WrapperHandle.ShouldBe(0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
rt.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review r1 M1: when the handle cannot be read (a renamed wrapper field), the failed tag is ROOTED — its callback
|
||||
/// delegate can then never be collected — and nothing is destroyed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Without_a_readable_handle_the_failed_tag_is_rooted_instead()
|
||||
{
|
||||
var rooted = LibplctagOrphanRelease.RootedCount;
|
||||
var released = LibplctagOrphanRelease.Released;
|
||||
var rt = new LibplctagTagRuntime(Refused());
|
||||
LibplctagOrphanRelease.ForceRootingForTest = true;
|
||||
try
|
||||
{
|
||||
await Should.ThrowAsync<AbTransportException>(() => rt.InitializeAsync(CancellationToken.None));
|
||||
LibplctagOrphanRelease.RootedCount.ShouldBe(rooted + 1);
|
||||
LibplctagOrphanRelease.Released.ShouldBe(released);
|
||||
}
|
||||
finally
|
||||
{
|
||||
LibplctagOrphanRelease.ForceRootingForTest = false;
|
||||
// Clean up the native tag this test deliberately leaked (its callback was unregistered by the timeout).
|
||||
var handle = rt.NativeHandle;
|
||||
if (handle > 0) plctag.plc_tag_destroy(handle);
|
||||
rt.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>plc_tag_lock answers PLCTAG_ERR_NOT_FOUND for a handle the library no longer holds.</summary>
|
||||
internal static bool NativeTagExists(int handle)
|
||||
{
|
||||
if (plctag.plc_tag_lock(handle) != 0) return false;
|
||||
plctag.plc_tag_unlock(handle);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
using libplctag.NativeImport;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// T21 press pass 1, AbLegacy side: a failed libplctag initialisation must destroy the native tag it created
|
||||
/// (libplctag.NET 1.5.2's <c>Dispose()</c> skips an uninitialised tag, leaving a native tag whose callback can fire
|
||||
/// into a collected delegate and kill the process). See <c>LibplctagOrphanRelease</c>.
|
||||
/// </summary>
|
||||
public sealed class LibplctagLegacyOrphanReleaseTests
|
||||
{
|
||||
[Fact]
|
||||
public void The_pinned_libplctag_wrapper_still_exposes_the_native_handle() =>
|
||||
LibplctagOrphanRelease.CanReadHandles.ShouldBeTrue();
|
||||
|
||||
[Fact]
|
||||
public async Task A_failed_initialisation_destroys_the_native_tag_it_created()
|
||||
{
|
||||
var rt = new LibplctagLegacyTagRuntime(new AbLegacyTagCreateParams(
|
||||
Gateway: "127.0.0.1", Port: 1, CipPath: "1,0", LibplctagPlcAttribute: "slc500",
|
||||
TagName: "N7:0", Timeout: TimeSpan.FromMilliseconds(300)));
|
||||
try
|
||||
{
|
||||
await Should.ThrowAsync<AbTransportException>(() => rt.InitializeAsync(CancellationToken.None));
|
||||
var handle = rt.NativeHandle;
|
||||
handle.ShouldBeGreaterThan(0, "plc_tag_create_ex returned a handle before the failure");
|
||||
var exists = plctag.plc_tag_lock(handle) == 0;
|
||||
if (exists) plctag.plc_tag_unlock(handle);
|
||||
exists.ShouldBeFalse("the failed tag's native handle must be destroyed, not orphaned");
|
||||
rt.GetStatus().ShouldNotBe((int)AbStatus.NotFound);
|
||||
rt.WrapperHandle.ShouldBe(0); // review r1 M2: a repeat release cannot touch the destroyed id
|
||||
}
|
||||
finally
|
||||
{
|
||||
rt.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user