Skip to content

Commit e93194e

Browse files
authored
Feature/more debug logging to LogMatcher (#1110)
* add debug logging, some refactor * bugfix * add newsfrag
1 parent f41c330 commit e93194e

File tree

8 files changed

+193
-59
lines changed

8 files changed

+193
-59
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add more logging to :py:meth:`~testplan.common.utils.match.LogMatcher.match`, :py:meth:`~testplan.common.utils.match.LogMatcher.not_match` and :py:meth:`~testplan.common.utils.match.LogMatcher.match_all` for debugging purpose.

examples/Assertions/Basic/test_plan_logfile.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ def logpath(self):
3535
def starting(self):
3636
super().starting()
3737
self._log = open(self.logpath, "wb+")
38+
self._log.write(b"pretending setup line\n")
39+
self._log.flush()
3840
self._sig = threading.Event()
3941
self._thr = threading.Thread(target=self._start_loop)
4042
self._thr.start()
@@ -93,9 +95,9 @@ def match_logfile(self, env, result):
9395
def match_logfile_xfail(self, env, result):
9496
lm = LogMatcher(env.server.logpath, binary=True)
9597
with result.logfile.expect(
96-
lm, r"pong", description="first assertion", timeout=0.2
98+
lm, r"pong", description="first assertion", timeout=1
9799
):
98-
env.client.send(b"ping")
100+
env.client.send(b"ping\n")
99101

100102

101103
@test_plan(

testplan/common/utils/match.py

Lines changed: 125 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@
66
import time
77
import warnings
88
from contextlib import closing
9-
from itertools import repeat
109
from typing import (
1110
AnyStr,
1211
Dict,
13-
Iterator,
1412
List,
1513
Match,
1614
Optional,
@@ -41,6 +39,14 @@
4139
Regex: TypeAlias = Union[str, bytes, Pattern]
4240

4341

42+
def _format_logline(s):
43+
if not s:
44+
return "<EOF>\n"
45+
if len(s) <= 100:
46+
return s
47+
return f"{s[:100]} ... ({len(s) - 100} chars omitted)"
48+
49+
4450
def match_regexps_in_file(
4551
logpath: os.PathLike, log_extracts: List[Pattern]
4652
) -> Tuple[bool, Dict[str, str], List[Pattern]]:
@@ -109,11 +115,11 @@ def __enter__(self):
109115
def __exit__(self, exc_type, exc_value, traceback):
110116
if exc_type is not None:
111117
return False
112-
s_pos = self.log_matcher.position
113118
m = self.log_matcher.match(
114119
self.regex, self.timeout, raise_on_timeout=False
115120
)
116-
e_pos = self.log_matcher.position
121+
s_pos = self.log_matcher._debug_info_s[0]
122+
e_pos = self.log_matcher._debug_info_e[0]
117123
if m is not None:
118124
self.match_results.append((m, self.regex, s_pos, e_pos))
119125
else:
@@ -142,6 +148,9 @@ def __init__(
142148
self.position: Optional[LogPosition] = None
143149
self.log_stream: FileLogStream = self._create_log_stream()
144150

151+
self._debug_info_s = ()
152+
self._debug_info_e = ()
153+
145154
# deprecation helpers
146155
self.had_transformed = False
147156

@@ -241,41 +250,44 @@ def mark(self, name: str):
241250
"""
242251
self.marks[name] = self.position
243252

244-
def match(
253+
def _match(
245254
self,
246-
regex: Regex,
247-
timeout: float = LOG_MATCHER_DEFAULT_TIMEOUT,
248-
raise_on_timeout: bool = True,
255+
regex: Pattern[AnyStr],
256+
timeout: float,
249257
) -> Optional[Match]:
250258
"""
251-
Matches each line in the log file from the current line number to the
252-
end of the file. If a match is found the line number is stored and the
253-
match is returned. By default an exception is raised if no match is
254-
found.
259+
Base block for ``match``, ``not_match`` & ``match_all``,
260+
as well as certain ``LogfileNamespace`` assertions.
255261
256-
:param regex: Regex string or compiled regular expression
257-
(``re.compile``)
262+
:param regex: Checked regular expression
258263
:param timeout: Timeout in seconds to wait for matching process,
259264
0 means matching till EOF and not waiting for new lines, any
260265
value greater than 0 means doing matching up to such seconds,
261266
defaults to 5 seconds
262-
:param raise_on_timeout: To raise TimeoutException or not
263267
:return: The regex match or None if no match is found
264268
"""
265269
match = None
266270
start_time = time.time()
267271
end_time = start_time + timeout
268-
269272
regex = self._prepare_regexp(regex)
270273

271274
with closing(self.log_stream) as log:
272275
log.seek(self.position)
273276

277+
non_eof = ""
274278
while True:
275-
if timeout > 0 and time.time() > end_time:
276-
break
277279
line = log.readline()
280+
if self._debug_info_s is None:
281+
self._debug_info_s = (
282+
str(self.position)
283+
if self.position is not None
284+
else "<BOF>",
285+
start_time,
286+
_format_logline(line),
287+
)
288+
278289
if line:
290+
non_eof = line
279291
match = regex.match(line)
280292
if match:
281293
break
@@ -284,20 +296,62 @@ def match(
284296
else:
285297
break
286298

299+
if timeout > 0 and time.time() > end_time:
300+
break
301+
287302
self.position = self.log_stream.position
303+
if self._debug_info_e is None:
304+
self._debug_info_e = (
305+
str(self.position),
306+
time.time(),
307+
_format_logline(non_eof),
308+
)
288309

289-
if match is not None:
310+
return match
311+
312+
def match(
313+
self,
314+
regex: Regex,
315+
timeout: float = LOG_MATCHER_DEFAULT_TIMEOUT,
316+
raise_on_timeout: bool = True,
317+
) -> Optional[Match]:
318+
"""
319+
Matches each line in the log file from the current line number to the
320+
end of the file. If a match is found the line number is stored and the
321+
match is returned. By default an exception is raised if no match is
322+
found.
323+
324+
:param regex: Regex string or compiled regular expression
325+
(``re.compile``)
326+
:param timeout: Timeout in seconds to wait for matching process,
327+
0 means matching till EOF and not waiting for new lines, any
328+
value greater than 0 means doing matching up to such seconds,
329+
defaults to 5 seconds
330+
:param raise_on_timeout: To raise TimeoutException or not
331+
:return: The regex match or None if no match is found
332+
"""
333+
self._debug_info_s = None
334+
self._debug_info_e = None
335+
regex = self._prepare_regexp(regex)
336+
337+
m = self._match(regex, timeout=timeout)
338+
339+
if m is None:
290340
self.logger.debug(
291-
"Match[%s] found in %.2fs",
341+
"%s: no expected match[%s] found,\nsearch starting from %s (around %s), "
342+
"where first line seen as:\n%s"
343+
"and ending at %s (around %s), where last line seen as:\n%s",
344+
self,
292345
regex.pattern,
293-
time.time() - start_time,
294-
)
295-
elif timeout and raise_on_timeout:
296-
raise timing.TimeoutException(
297-
"No match[{}] found in {}s".format(regex.pattern, timeout)
346+
*self._debug_info_s,
347+
*self._debug_info_e,
298348
)
349+
if timeout and raise_on_timeout:
350+
raise timing.TimeoutException(
351+
"No match[%s] found in %.2fs.", regex.pattern, timeout
352+
)
299353

300-
return match
354+
return m
301355

302356
def not_match(
303357
self,
@@ -316,9 +370,22 @@ def not_match(
316370
0 means should not wait and return whatever matched on initial
317371
scan, defaults to 5 seconds
318372
"""
373+
self._debug_info_s = None
374+
self._debug_info_e = None
375+
regex = self._prepare_regexp(regex)
376+
377+
m = self._match(regex, timeout)
319378

320-
match = self.match(regex, timeout, raise_on_timeout=False)
321-
if match is not None:
379+
if m is not None:
380+
self.logger.debug(
381+
"%s: unexpected match[%s] found,\nsearch starting from %s (around %s), "
382+
"where first line seen as:\n%s"
383+
"and ending at %s (around %s), where last line seen as:\n%s",
384+
self,
385+
regex.pattern,
386+
*self._debug_info_s,
387+
*self._debug_info_e,
388+
)
322389
raise Exception(
323390
f"Unexpected match[{regex.pattern}] found in {timeout}s"
324391
)
@@ -343,15 +410,37 @@ def match_all(
343410
matches = []
344411
end_time = time.time() + timeout
345412

346-
try:
347-
while timeout >= 0:
348-
matches.append(
349-
self.match(regex, timeout, raise_on_timeout=True)
413+
self._debug_info_s = None
414+
regex = self._prepare_regexp(regex)
415+
416+
while True:
417+
if timeout == 0:
418+
t = 0
419+
else:
420+
t = end_time - time.time()
421+
if t <= 0:
422+
break
423+
self._debug_info_e = None
424+
m = self._match(regex, t)
425+
if m is not None:
426+
matches.append(m)
427+
else:
428+
break
429+
430+
if not matches:
431+
self.logger.debug(
432+
"%s: no expected match[%s] found,\nsearch starting from %s (around %s), "
433+
"where first line seen as:\n%s"
434+
"and ending at %s (around %s), where last line seen as:\n%s",
435+
self,
436+
regex.pattern,
437+
*self._debug_info_s,
438+
*self._debug_info_e,
439+
)
440+
if timeout and raise_on_timeout:
441+
raise timing.TimeoutException(
442+
"No match[%s] found in %.2fs.", regex.pattern, timeout
350443
)
351-
timeout = end_time - time.time()
352-
except timing.TimeoutException:
353-
if not matches and raise_on_timeout:
354-
raise
355444

356445
return matches
357446

testplan/testing/multitest/entries/assertions.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,7 +1486,7 @@ class LogfileMatch(Assertion):
14861486
"""
14871487

14881488
@dataclass
1489-
class Result:
1489+
class ResultRep:
14901490
matched: Optional[str]
14911491
pattern: str
14921492
start_pos: str
@@ -1519,14 +1519,12 @@ def _truncate_str(s):
15191519
@classmethod
15201520
def _handle_quadruple(cls, tup):
15211521
m, r, s, e = tup
1522-
if s is None:
1523-
s = "<BOF>"
15241522
r = re.compile(r).pattern
1525-
return cls.Result(
1523+
return cls.ResultRep(
15261524
cls._truncate_str(m.group()) if m is not None else None,
15271525
cls._truncate_str(r),
1528-
str(s),
1529-
str(e),
1526+
s,
1527+
e,
15301528
)
15311529

15321530
def evaluate(self):

testplan/testing/result.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1450,9 +1450,9 @@ def match(
14501450
"""
14511451
results = []
14521452
failure = None
1453-
s_pos = log_matcher.position
14541453
m = log_matcher.match(regex, timeout, raise_on_timeout=False)
1455-
e_pos = log_matcher.position
1454+
s_pos = log_matcher._debug_info_s[0]
1455+
e_pos = log_matcher._debug_info_e[0]
14561456
if m is not None:
14571457
results.append((m, regex, s_pos, e_pos))
14581458
else:

testplan/web_ui/testing/src/AssertionPane/AssertionTypes/LogfileMatchAssertion.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const LogfileMatchAssertion = ({ assertion }) => {
3636
const { timeout, results, failure } = assertion;
3737

3838
const timeoutMsg =
39-
(assertion.passed ? "Passed" : "Failed") + ` in ${timeout} seconds.`;
39+
(assertion.passed ? "Passed" : "Failed") + (timeout === 0 ? " when scanning till <EOF>." : ` in ${timeout} seconds.`);
4040
const entries = [...results, ...failure].map((entry, index) => {
4141
const { matched, pattern, start_pos: startPos, end_pos: endPos } = entry;
4242
return (

0 commit comments

Comments
 (0)