11import logging
22import json
3+ import signal
34from pathlib import Path
45from enum import Enum , StrEnum , auto
56from typing import List , NamedTuple , TypeVar , Type , Dict , Any , Set , Tuple
1011
1112
1213# Supported operations
13- SUPPORTED_OPERATIONS = {"run" , "wait" , "read" , "write" , "connect" , "send" , "recv" }
14+ SUPPORTED_OPERATIONS = {"run" , "wait" , "read" , "write" , "connect" ,
15+ "send" , "recv" , "request" , "kill" }
1416
1517
1618class WasiVersion (StrEnum ):
@@ -166,14 +168,104 @@ def from_config(cls: Type[C], config: Dict[str, Any]) -> C:
166168 )
167169
168170
169- Operation = Run | Wait | Read | Write | Connect | Send | Recv
171+ Resp = TypeVar ("Resp" , bound = "Response" )
172+
173+
174+ class Response (NamedTuple ):
175+ status : int
176+ headers : Dict [str , str ]
177+ body : str
178+
179+ @classmethod
180+ def from_config (cls : Type [Resp ], config : Dict [str , Any ]) -> Resp :
181+ status = config .get ("status" , 200 )
182+ headers = config .get ("headers" , {})
183+ body = config .get ("body" , "" )
184+
185+ if not isinstance (status , int ):
186+ raise ValueError ("Response status code should be an int" )
187+ if not isinstance (headers , dict ):
188+ raise ValueError ("Response expected headers should be a dict" )
189+ for k , v in headers .items ():
190+ if not isinstance (k , str ):
191+ raise ValueError ("Response expected header name should be a str" )
192+ if not isinstance (v , str ):
193+ raise ValueError ("Response expected header value should be a str" )
194+ if not isinstance (body , str ):
195+ raise ValueError ("Response expected body should be a str" )
196+
197+ return cls (status , headers , body )
198+
199+
200+ Req = TypeVar ("Req" , bound = "Request" )
201+
202+
203+ class Request (NamedTuple ):
204+ method : str
205+ path : str
206+ response : Response
207+
208+ @classmethod
209+ def from_config (cls : Type [Req ], config : Dict [str , Any ]) -> Req :
210+ method = config .get ("method" , "GET" )
211+ path = config .get ("path" , "/" )
212+ response = config .get ("response" , {})
213+
214+ if not isinstance (method , str ):
215+ raise ValueError ("Request method should be a str" )
216+ if not isinstance (path , str ):
217+ raise ValueError ("Request path should be a str" )
218+ response = Response .from_config (response )
219+
220+ return cls (method , path , response )
221+
222+
223+ K = TypeVar ("K" , bound = "Kill" )
224+
225+
226+ class Kill (NamedTuple ):
227+ signal : signal .Signals
228+
229+ @classmethod
230+ def from_config (cls : Type [K ], config : Dict [str , Any ]) -> K :
231+ signame = config .get ("signal" , "SIGTERM" )
232+
233+ if not isinstance (signame , str ):
234+ raise ValueError (f"Signal name should be a str: { signame } " )
235+ if signame not in signal .Signals .__members__ :
236+ raise ValueError (f"Unknown signal name: { signame } " )
237+
238+ return cls (signal .Signals [signame ])
239+
240+
241+ Operation = Run | Wait | Read | Write | Connect | Send | Recv | Request | Kill
170242
171243
172244class WasiProposal (StrEnum ):
173245 HTTP = 'http'
246+ HTTP_SERVICE = 'http/service'
174247 SOCKETS = 'sockets'
175248
176249
250+ def _infer_proposals_from_operations (ops : List [Operation ]) -> List [WasiProposal ]:
251+ sockets = False
252+ http_service = False
253+ for op in ops :
254+ match op :
255+ case Recv () | Send () | Connect ():
256+ sockets = True
257+ case Request ():
258+ http_service = True
259+ case _:
260+ pass
261+ ret = []
262+ if sockets :
263+ ret .append (WasiProposal .SOCKETS )
264+ if http_service :
265+ ret .append (WasiProposal .HTTP_SERVICE )
266+ return ret
267+
268+
177269T = TypeVar ("T" , bound = "Config" )
178270
179271
@@ -196,8 +288,9 @@ def from_file(cls: Type[T], config_file: str) -> T:
196288 if dict_config .get ("operations" ) is not None :
197289 operations = cls ._operations_from_config (test_config_path , dict_config .get ("operations" ))
198290
199- proposals = []
200- if dict_config .get ("proposals" ) is not None :
291+ if dict_config .get ("proposals" ) is None :
292+ proposals = _infer_proposals_from_operations (operations )
293+ else :
201294 proposals = cls ._proposals_from_config (dict_config .get ("proposals" ))
202295
203296 return cls (operations = operations , proposals = proposals )
@@ -276,6 +369,10 @@ def _operations_from_config(cls: Type[T], test_config_path: Path, ops: List[Any]
276369 operations .append (Send .from_config (op ))
277370 case "recv" :
278371 operations .append (Recv .from_config (op ))
372+ case "request" :
373+ operations .append (Request .from_config (op ))
374+ case "kill" :
375+ operations .append (Kill .from_config (op ))
279376
280377 return operations
281378
@@ -321,6 +418,12 @@ def do_send(self, send: Send) -> None:
321418 def do_recv (self , recv : Recv ) -> None :
322419 raise NotImplementedError ()
323420
421+ def do_request (self , req : Request ) -> None :
422+ raise NotImplementedError ()
423+
424+ def do_kill (self , kill : Kill ) -> None :
425+ raise NotImplementedError ()
426+
324427 def do_cleanup (self , successful : bool ) -> None :
325428 raise NotImplementedError ()
326429
@@ -345,27 +448,33 @@ def run(self) -> Result:
345448 # wasi_test_runner/runtime_adapter.py:131: error: Argument 2 to "_handle_read"
346449 # has incompatible type "Read"; expected "Read" [arg-type]
347450 match op :
348- case Run () as run :
349- assert isinstance (run , Run )
350- self .do_run (run )
351- case Write () as write :
352- assert isinstance (write , Write )
353- self .do_write (write )
354- case Read () as read :
355- assert isinstance (read , Read )
356- self .do_read (read )
357- case Wait () as wait :
358- assert isinstance (wait , Wait )
359- self .do_wait (wait )
360- case Connect () as conn :
361- assert isinstance (conn , Connect )
362- self .do_connect (conn )
363- case Send () as send :
364- assert isinstance (send , Send )
365- self .do_send (send )
366- case Recv () as recv :
367- assert isinstance (recv , Recv )
368- self .do_recv (recv )
451+ case Run ():
452+ assert isinstance (op , Run )
453+ self .do_run (op )
454+ case Write ():
455+ assert isinstance (op , Write )
456+ self .do_write (op )
457+ case Read ():
458+ assert isinstance (op , Read )
459+ self .do_read (op )
460+ case Wait ():
461+ assert isinstance (op , Wait )
462+ self .do_wait (op )
463+ case Connect ():
464+ assert isinstance (op , Connect )
465+ self .do_connect (op )
466+ case Send ():
467+ assert isinstance (op , Send )
468+ self .do_send (op )
469+ case Recv ():
470+ assert isinstance (op , Recv )
471+ self .do_recv (op )
472+ case Request ():
473+ assert isinstance (op , Request )
474+ self .do_request (op )
475+ case Kill ():
476+ assert isinstance (op , Kill )
477+ self .do_kill (op )
369478
370479 successful = not self .has_failure ()
371480 finally :
@@ -445,6 +554,12 @@ def do_recv(self, recv: Recv) -> None:
445554 self .assert_proc (recv )
446555 self .assert_stream (recv , recv .id , StreamType .SOCKET )
447556
557+ def do_request (self , req : Request ) -> None :
558+ self .assert_proc (req )
559+
560+ def do_kill (self , kill : Kill ) -> None :
561+ self .assert_proc (kill )
562+
448563 def do_cleanup (self , successful : bool ) -> None :
449564 if successful :
450565 self .assert_no_proc (self ._config_path )
0 commit comments