11#!/usr/bin/env python3
2- """Launch the Qt simulator and prove GUI-applied state is visible over MMS."""
2+ """Prove full SCL materialization and GUI-applied state are visible over MMS."""
33
44from __future__ import annotations
55
@@ -29,14 +29,51 @@ def resolve_read_probe(argument: str) -> str:
2929 if path .is_dir ():
3030 names = {"ariec61850_mms_read_probe" , "ariec61850_mms_read_probe.exe" }
3131 matches = sorted (
32- candidate for candidate in path .rglob ("ariec61850_mms_read_probe*" )
32+ candidate
33+ for candidate in path .rglob ("ariec61850_mms_read_probe*" )
3334 if candidate .is_file () and candidate .name in names
3435 )
3536 if matches :
3637 return str (matches [0 ])
3738 raise FileNotFoundError (f"MMS read probe not found under { path } " )
3839
3940
41+ def run_probe (read_probe : str , port : int , item : str , * , type_only : bool = False ) -> subprocess .CompletedProcess [str ]:
42+ command = [
43+ read_probe ,
44+ "127.0.0.1" ,
45+ str (port ),
46+ "--domain" ,
47+ "MU01LD0" ,
48+ "--item" ,
49+ item ,
50+ "--timeout-ms" ,
51+ "3000" ,
52+ ]
53+ if type_only :
54+ command .append ("--type" )
55+ return subprocess .run (
56+ command ,
57+ capture_output = True ,
58+ text = True ,
59+ timeout = 6 ,
60+ check = False ,
61+ creationflags = creation_flags (),
62+ )
63+
64+
65+ def require_probe (
66+ probe : subprocess .CompletedProcess [str ],
67+ expected : str ,
68+ description : str ,
69+ ) -> None :
70+ if probe .returncode != 0 or expected not in probe .stdout :
71+ raise RuntimeError (
72+ f"{ description } failed: exit={ probe .returncode } "
73+ f"stdout={ probe .stdout } stderr={ probe .stderr } "
74+ )
75+
76+
4077def main () -> int :
4178 parser = argparse .ArgumentParser ()
4279 parser .add_argument ("--app" , required = True )
@@ -60,7 +97,7 @@ def main() -> int:
6097 "--set-first-value" ,
6198 "42" ,
6299 "--exit-after-ms" ,
63- "12000 " ,
100+ "30000 " ,
64101 ],
65102 stdout = app_log ,
66103 stderr = subprocess .STDOUT ,
@@ -73,7 +110,7 @@ def main() -> int:
73110 manifest_path = (
74111 Path (tempfile .gettempdir ()) / f"arstack-ied-simulator-{ app .pid } .model"
75112 )
76- manifest_deadline = time .monotonic () + 6 .0
113+ manifest_deadline = time .monotonic () + 8 .0
77114 while time .monotonic () < manifest_deadline :
78115 if app .poll () is not None :
79116 last_error = f"application exited early with code { app .returncode } "
@@ -82,64 +119,146 @@ def main() -> int:
82119 manifest_text = manifest_path .read_text (encoding = "utf-8" )
83120 except (FileNotFoundError , PermissionError , UnicodeDecodeError ):
84121 manifest_text = ""
85- if manifest_text .startswith ("ARSTACK_IED_MODEL\t 2\t 2\n " ) and (
86- "TCTR1$MX$Amp$instMag$i\t INT32\t Number\t 42" in manifest_text
122+ gui_value_present = (
123+ "TCTR1$MX$Amp$instMag$i\t INT16\t Number\t integer:16\t 42"
124+ in manifest_text
125+ )
126+ ordered_range_present = (
127+ "TCTR1$MX$Amp$instMag$range\t INT8U\t Number\t unsigned-integer:8\t 0"
128+ in manifest_text
129+ )
130+ # XCBR1.Health is deliberately absent from every DataSet. Its presence
131+ # proves DataTypeTemplates materialization, not FCDA-driven projection.
132+ structural_only_leaf_present = (
133+ "XCBR1$ST$Health$stVal\t BOOLEAN\t Boolean\t boolean\t false"
134+ in manifest_text
135+ )
136+ data_set_present = (
137+ "DS\t MU01LD0\t LLN0$dsSV\t MU01LD0\t TCTR1$MX$Amp$instMag$i"
138+ in manifest_text
139+ )
140+ rcb_present = (
141+ "RCB\t MU01LD0\t LLN0$RP$urcb01\t 0\t MU01_LD0_URCB01\t "
142+ "MU01LD0\t LLN0$dsSV\t 7\t 20\t 1000\t 0" in manifest_text
143+ )
144+ no_ln_scaffold = "\n LN\t " not in "\n " + manifest_text
145+ order_preserved = (
146+ "TCTR1$MX$Amp$instMag$i" in manifest_text
147+ and "TCTR1$MX$Amp$instMag$range" in manifest_text
148+ and manifest_text .index ("TCTR1$MX$Amp$instMag$i" )
149+ < manifest_text .index ("TCTR1$MX$Amp$instMag$range" )
150+ )
151+ if (
152+ manifest_text .startswith ("ARSTACK_IED_MODEL\t 4\t 2\n " )
153+ and gui_value_present
154+ and ordered_range_present
155+ and structural_only_leaf_present
156+ and data_set_present
157+ and rcb_present
158+ and no_ln_scaffold
159+ and order_preserved
87160 ):
88161 break
89162 time .sleep (0.1 )
90163 else :
91- raise RuntimeError ("GUI did not publish manifest revision 2 with value 42" )
164+ raise RuntimeError (
165+ "GUI did not publish the complete v4 P0 model with exact types, "
166+ "DataSet, RCB, and structural-only leaves"
167+ )
92168
93- deadline = time .monotonic () + 10 .0
169+ deadline = time .monotonic () + 15 .0
94170 while time .monotonic () < deadline :
95171 if app .poll () is not None :
96172 last_error = f"application exited early with code { app .returncode } "
97173 break
98174 time .sleep (0.25 )
99- probe = subprocess .run (
100- [
101- read_probe ,
102- "127.0.0.1" ,
103- str (port ),
104- "--domain" ,
105- "MU01LD0" ,
106- "--item" ,
107- "TCTR1$MX$Amp$instMag$i" ,
108- "--timeout-ms" ,
109- "3000" ,
110- ],
111- capture_output = True ,
112- text = True ,
113- timeout = 6 ,
114- check = False ,
115- creationflags = creation_flags (),
116- )
117- if probe .returncode == 0 and "value=42" in probe .stdout :
118- app .wait (timeout = 14 )
119- print (
120- "IEDSIM_GUI_LIVE_VALUE_PASS "
121- "reference=MU01LD0/TCTR1$MX$Amp$instMag$i value=42"
175+ edited_probe = run_probe (
176+ read_probe , port , "TCTR1$MX$Amp$instMag$i"
177+ )
178+ if edited_probe .returncode != 0 or "value=42" not in edited_probe .stdout :
179+ last_error = (
180+ f"edited read exit={ edited_probe .returncode } "
181+ f"stdout={ edited_probe .stdout } stderr={ edited_probe .stderr } "
122182 )
123- return 0
124- last_error = (
125- f"exit={ probe .returncode } stdout={ probe .stdout } stderr={ probe .stderr } "
183+ time .sleep (0.2 )
184+ continue
185+
186+ # P0 exact TypeSpecification is proven on the wire with
187+ # GetVariableAccessAttributes, not inferred from manifest text.
188+ exact_type_probe = run_probe (
189+ read_probe ,
190+ port ,
191+ "TCTR1$MX$Amp$instMag$i" ,
192+ type_only = True ,
126193 )
127- time .sleep (0.2 )
194+ require_probe (
195+ exact_type_probe ,
196+ "kind=integer size=16" ,
197+ "GVAA INT16 evidence" ,
198+ )
199+ exact_unsigned_probe = run_probe (
200+ read_probe ,
201+ port ,
202+ "TCTR1$MX$Amp$instMag$range" ,
203+ type_only = True ,
204+ )
205+ require_probe (
206+ exact_unsigned_probe ,
207+ "kind=unsigned size=8" ,
208+ "GVAA INT8U evidence" ,
209+ )
210+
211+ structural_probe = run_probe (
212+ read_probe , port , "XCBR1$ST$Health$stVal"
213+ )
214+ require_probe (
215+ structural_probe ,
216+ "value=false" ,
217+ "structural-only leaf read" ,
218+ )
219+
220+ # The SCL ReportControl is part of the P0 structural model even
221+ # though RptEna/GI/report emission are intentionally deferred to P2.
222+ rcb_probe = run_probe (
223+ read_probe , port , "LLN0$RP$urcb01$RptID"
224+ )
225+ require_probe (
226+ rcb_probe ,
227+ "value=MU01_LD0_URCB01" ,
228+ "URCB RptID read" ,
229+ )
230+ rcb_dataset_probe = run_probe (
231+ read_probe , port , "LLN0$RP$urcb01$DatSet"
232+ )
233+ require_probe (
234+ rcb_dataset_probe ,
235+ "value=MU01LD0/LLN0$dsSV" ,
236+ "URCB DataSet binding read" ,
237+ )
238+
239+ app .wait (timeout = 32 )
240+ print (
241+ "IEDSIM_P0_FULL_MODEL_PASS "
242+ "edited=MU01LD0/TCTR1$MX$Amp$instMag$i:42 "
243+ "gvaa=integer:16,unsigned:8 dataset=LLN0$dsSV "
244+ "rcb=LLN0$RP$urcb01 structural=MU01LD0/XCBR1$ST$Health$stVal:false"
245+ )
246+ return 0
128247 raise RuntimeError (last_error )
129- except BaseException :
248+ except BaseException as exception :
249+ last_error = str (exception ) or last_error
130250 if app .poll () is None :
131251 try :
132- # Let Qt destroy its QProcess child and release the listener.
133252 app .wait (timeout = 4 )
134253 except subprocess .TimeoutExpired :
135254 app .kill ()
136255 app .wait (timeout = 5 )
137256 app_log .seek (0 )
138257 output = app_log .read ().strip ()
139258 raise RuntimeError (
140- f"GUI live-value test failed: { last_error } ; app_output={ output } "
141- )
259+ f"GUI full-model/ live-value test failed: { last_error } ; app_output={ output } "
260+ ) from exception
142261
143262
144263if __name__ == "__main__" :
145- raise SystemExit (main ())
264+ raise SystemExit (main ())
0 commit comments