88"""Utility functions useful when using munet testing functionailty in pytest."""
99import asyncio
1010import datetime
11+ import fcntl
1112import functools
1213import logging
14+ import os
15+ import re
16+ import select
1317import sys
1418import time
1519
1620from ..base import BaseMunet
21+ from ..base import Timeout
1722from ..cli import async_cli
1823
1924
2328
2429
2530async def async_pause_test (desc = "" ):
31+ """Pause the running of a test offering options for CLI or PDB."""
2632 isatty = sys .stdout .isatty ()
2733 if not isatty :
2834 desc = f" for { desc } " if desc else ""
@@ -49,11 +55,12 @@ async def async_pause_test(desc=""):
4955
5056
5157def pause_test (desc = "" ):
58+ """Pause the running of a test offering options for CLI or PDB."""
5259 asyncio .run (async_pause_test (desc ))
5360
5461
5562def retry (retry_timeout , initial_wait = 0 , retry_sleep = 2 , expected = True ):
56- """decorator: retry while functions return is not None or raises an exception.
63+ """Retry decorated function until it returns None, raises an exception, or timeout .
5764
5865 * `retry_timeout`: Retry for at least this many seconds; after waiting
5966 initial_wait seconds
@@ -116,3 +123,91 @@ def func_retry(*args, **kwargs):
116123 return func_retry
117124
118125 return _retry
126+
127+
128+ def readline (f , timeout = None ):
129+ """Read a line or timeout.
130+
131+ This function will take over the file object, the file object should not be used
132+ outside of calling this function once you begin.
133+
134+ Return: A line, remaining buffer if EOF (subsequent calls will return ""), or None
135+ for timeout.
136+ """
137+ fd = f .fileno ()
138+ if not hasattr (f , "munet_non_block_set" ):
139+ flags = fcntl .fcntl (fd , fcntl .F_GETFL )
140+ fcntl .fcntl (fd , fcntl .F_SETFL , flags | os .O_NONBLOCK )
141+ f .munet_non_block_set = True
142+ f .munet_lines = []
143+ f .munet_buf = ""
144+
145+ if f .munet_lines :
146+ return f .munet_lines .pop (0 )
147+
148+ timeout = Timeout (timeout )
149+ remaining = timeout .remaining ()
150+ while remaining > 0 :
151+ ready , _ , _ = select .select ([fd ], [], [], remaining )
152+ if not ready :
153+ return None
154+
155+ c = f .read ()
156+ if c is None :
157+ logging .error ("munet readline: unexpected None during read" )
158+ return None
159+
160+ if not c :
161+ logging .debug ("munet readline: got eof" )
162+ c = f .munet_buf
163+ f .munet_buf = ""
164+ return c
165+
166+ f .munet_buf += c
167+ while "\n " in f .munet_buf :
168+ a , f .munet_buf = f .munet_buf .split ("\n " , 1 )
169+ f .munet_lines .append (a + "\n " )
170+
171+ if f .munet_lines :
172+ return f .munet_lines .pop (0 )
173+
174+ remaining = timeout .remaining ()
175+ return None
176+
177+
178+ def waitline (f , regex , timeout = 120 ):
179+ """Match a regex within lines from a file with a timeout.
180+
181+ This function will take over the file object (by calling `readline` above), the file
182+ object should not be used outside of calling these functions once you begin.
183+
184+ Return: the match object or None.
185+ """
186+ timeo = Timeout (timeout )
187+ while not timeo .is_expired ():
188+ line = readline (f , timeo .remaining ())
189+ if line is None :
190+ break
191+
192+ if line == "" :
193+ logging .warning ("waitline: got eof while matching '%s'" , regex )
194+ return None
195+
196+ assert line [- 1 ] == "\n "
197+ line = line [:- 1 ]
198+ if not line :
199+ continue
200+
201+ logging .debug ("waitline: searching: '%s' for '%s'" , line , regex )
202+ m = re .search (regex , line )
203+ if m :
204+ logging .debug ("waitline: matched '%s'" , m .group (0 ))
205+ return m
206+
207+ logging .warning (
208+ "Timeout while getting output matching '%s' within %ss (actual %ss)" ,
209+ regex ,
210+ timeout ,
211+ timeo .elapsed (),
212+ )
213+ return None
0 commit comments