mirror of
https://github.com/zulip/zulip.git
synced 2025-11-13 18:36:36 +00:00
test_console_output: Implement the entire TextIO contract.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
committed by
Tim Abbott
parent
702ce071f4
commit
d40f3d54f1
@@ -68,8 +68,8 @@ from zerver.lib.streams import (
|
|||||||
from zerver.lib.test_console_output import (
|
from zerver.lib.test_console_output import (
|
||||||
ExtraConsoleOutputFinder,
|
ExtraConsoleOutputFinder,
|
||||||
ExtraConsoleOutputInTestException,
|
ExtraConsoleOutputInTestException,
|
||||||
TeeStderrAndFindExtraConsoleOutput,
|
tee_stderr_and_find_extra_console_output,
|
||||||
TeeStdoutAndFindExtraConsoleOutput,
|
tee_stdout_and_find_extra_console_output,
|
||||||
)
|
)
|
||||||
from zerver.lib.test_helpers import find_key_by_email, instrument_url
|
from zerver.lib.test_helpers import find_key_by_email, instrument_url
|
||||||
from zerver.lib.users import get_api_key
|
from zerver.lib.users import get_api_key
|
||||||
@@ -168,9 +168,9 @@ class ZulipTestCase(TestCase):
|
|||||||
if not settings.BAN_CONSOLE_OUTPUT:
|
if not settings.BAN_CONSOLE_OUTPUT:
|
||||||
return super().run(result)
|
return super().run(result)
|
||||||
extra_output_finder = ExtraConsoleOutputFinder()
|
extra_output_finder = ExtraConsoleOutputFinder()
|
||||||
with TeeStderrAndFindExtraConsoleOutput(
|
with tee_stderr_and_find_extra_console_output(
|
||||||
extra_output_finder
|
extra_output_finder
|
||||||
), TeeStdoutAndFindExtraConsoleOutput(extra_output_finder):
|
), tee_stdout_and_find_extra_console_output(extra_output_finder):
|
||||||
test_result = super().run(result)
|
test_result = super().run(result)
|
||||||
if extra_output_finder.full_extra_output:
|
if extra_output_finder.full_extra_output:
|
||||||
exception_message = f"""
|
exception_message = f"""
|
||||||
@@ -189,7 +189,7 @@ You should be able to quickly reproduce this failure with:
|
|||||||
test-backend --ban-console-output {self.id()}
|
test-backend --ban-console-output {self.id()}
|
||||||
|
|
||||||
Output:
|
Output:
|
||||||
{extra_output_finder.full_extra_output}
|
{extra_output_finder.full_extra_output.decode(errors="replace")}
|
||||||
--------------------------------------------
|
--------------------------------------------
|
||||||
"""
|
"""
|
||||||
raise ExtraConsoleOutputInTestException(exception_message)
|
raise ExtraConsoleOutputInTestException(exception_message)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from io import SEEK_SET, TextIOWrapper
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import Optional, Sequence, Type, cast
|
from typing import IO, Iterable, Iterator, List, Optional, Type
|
||||||
|
|
||||||
|
|
||||||
class ExtraConsoleOutputInTestException(Exception):
|
class ExtraConsoleOutputInTestException(Exception):
|
||||||
@@ -13,106 +15,155 @@ class ExtraConsoleOutputFinder:
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
valid_line_patterns = [
|
valid_line_patterns = [
|
||||||
# Example: Running zerver.tests.test_attachments.AttachmentsTests.test_delete_unauthenticated
|
# Example: Running zerver.tests.test_attachments.AttachmentsTests.test_delete_unauthenticated
|
||||||
"^Running ",
|
b"^Running ",
|
||||||
# Example: ** Test is TOO slow: analytics.tests.test_counts.TestRealmActiveHumans.test_end_to_end (0.581 s)
|
# Example: ** Test is TOO slow: analytics.tests.test_counts.TestRealmActiveHumans.test_end_to_end (0.581 s)
|
||||||
"^\\*\\* Test is TOO slow: ",
|
b"^\\*\\* Test is TOO slow: ",
|
||||||
"^----------------------------------------------------------------------",
|
b"^----------------------------------------------------------------------",
|
||||||
# Example: INFO: URL coverage report is in var/url_coverage.txt
|
# Example: INFO: URL coverage report is in var/url_coverage.txt
|
||||||
"^INFO: URL coverage report is in",
|
b"^INFO: URL coverage report is in",
|
||||||
# Example: INFO: Try running: ./tools/create-test-api-docs
|
# Example: INFO: Try running: ./tools/create-test-api-docs
|
||||||
"^INFO: Try running:",
|
b"^INFO: Try running:",
|
||||||
# Example: -- Running tests in parallel mode with 4 processes
|
# Example: -- Running tests in parallel mode with 4 processes
|
||||||
"^-- Running tests in",
|
b"^-- Running tests in",
|
||||||
"^OK",
|
b"^OK",
|
||||||
# Example: Ran 2139 tests in 115.659s
|
# Example: Ran 2139 tests in 115.659s
|
||||||
"^Ran [0-9]+ tests in",
|
b"^Ran [0-9]+ tests in",
|
||||||
# Destroying test database for alias 'default'...
|
# Destroying test database for alias 'default'...
|
||||||
"^Destroying test database for alias ",
|
b"^Destroying test database for alias ",
|
||||||
"^Using existing clone",
|
b"^Using existing clone",
|
||||||
"^\\*\\* Skipping ",
|
b"^\\*\\* Skipping ",
|
||||||
]
|
]
|
||||||
self.compiled_line_patterns = []
|
self.compiled_line_pattern = re.compile(b"|".join(valid_line_patterns))
|
||||||
for pattern in valid_line_patterns:
|
self.partial_line = b""
|
||||||
self.compiled_line_patterns.append(re.compile(pattern))
|
self.full_extra_output = b""
|
||||||
self.full_extra_output = ""
|
|
||||||
|
|
||||||
def find_extra_output(self, data: str) -> None:
|
def find_extra_output(self, data: bytes) -> None:
|
||||||
lines = data.split("\n")
|
*lines, self.partial_line = (self.partial_line + data).split(b"\n")
|
||||||
for line in lines:
|
for line in lines:
|
||||||
if not line:
|
if not self.compiled_line_pattern.match(line):
|
||||||
continue
|
self.full_extra_output += line + b"\n"
|
||||||
found_extra_output = True
|
|
||||||
for compiled_pattern in self.compiled_line_patterns:
|
|
||||||
if compiled_pattern.match(line):
|
|
||||||
found_extra_output = False
|
|
||||||
break
|
|
||||||
if found_extra_output:
|
|
||||||
self.full_extra_output += f"{line}\n"
|
|
||||||
|
|
||||||
|
|
||||||
class TeeStderrAndFindExtraConsoleOutput:
|
class WrappedIO(IO[bytes]):
|
||||||
def __init__(self, extra_output_finder: ExtraConsoleOutputFinder) -> None:
|
def __init__(self, stream: IO[bytes], extra_output_finder: ExtraConsoleOutputFinder) -> None:
|
||||||
self.stderr_stream = sys.stderr
|
self.stream = stream
|
||||||
|
self.extra_output_finder = extra_output_finder
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mode(self) -> str:
|
||||||
|
return self.stream.mode
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return self.stream.name
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def closed(self) -> bool:
|
||||||
|
return self.stream.closed
|
||||||
|
|
||||||
|
def fileno(self) -> int:
|
||||||
|
return self.stream.fileno()
|
||||||
|
|
||||||
|
def flush(self) -> None:
|
||||||
|
self.stream.flush()
|
||||||
|
|
||||||
|
def isatty(self) -> bool:
|
||||||
|
return self.stream.isatty()
|
||||||
|
|
||||||
|
def read(self, n: int = -1) -> bytes:
|
||||||
|
return self.stream.read(n)
|
||||||
|
|
||||||
|
def readable(self) -> bool:
|
||||||
|
return self.stream.readable()
|
||||||
|
|
||||||
|
def readline(self, limit: int = -1) -> bytes:
|
||||||
|
return self.stream.readline(limit)
|
||||||
|
|
||||||
|
def readlines(self, hint: int = -1) -> List[bytes]:
|
||||||
|
return self.stream.readlines(hint)
|
||||||
|
|
||||||
|
def seek(self, offset: int, whence: int = SEEK_SET) -> int:
|
||||||
|
return self.stream.seek(offset, whence)
|
||||||
|
|
||||||
|
def seekable(self) -> bool:
|
||||||
|
return self.stream.seekable()
|
||||||
|
|
||||||
|
def tell(self) -> int:
|
||||||
|
return self.stream.tell()
|
||||||
|
|
||||||
|
def truncate(self, size: Optional[int] = None) -> int:
|
||||||
|
return self.truncate(size)
|
||||||
|
|
||||||
|
def writable(self) -> bool:
|
||||||
|
return self.stream.writable()
|
||||||
|
|
||||||
|
def write(self, data: bytes) -> int:
|
||||||
|
num_chars = self.stream.write(data)
|
||||||
|
self.extra_output_finder.find_extra_output(data)
|
||||||
|
return num_chars
|
||||||
|
|
||||||
|
def writelines(self, data: Iterable[bytes]) -> None:
|
||||||
|
self.stream.writelines(data)
|
||||||
|
lines = b"".join(data)
|
||||||
|
self.extra_output_finder.find_extra_output(lines)
|
||||||
|
|
||||||
|
def __next__(self) -> bytes:
|
||||||
|
return next(self.stream)
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[bytes]:
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __enter__(self) -> IO[bytes]:
|
||||||
|
self.stream.__enter__()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(
|
||||||
|
self,
|
||||||
|
exc_type: Optional[Type[BaseException]],
|
||||||
|
exc_value: Optional[BaseException],
|
||||||
|
traceback: Optional[TracebackType],
|
||||||
|
) -> Optional[bool]:
|
||||||
|
return self.stream.__exit__(exc_type, exc_value, traceback)
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def tee_stderr_and_find_extra_console_output(
|
||||||
|
extra_output_finder: ExtraConsoleOutputFinder,
|
||||||
|
) -> Iterator[None]:
|
||||||
|
stderr = sys.stderr
|
||||||
|
|
||||||
# get shared console handler instance from any logger that have it
|
# get shared console handler instance from any logger that have it
|
||||||
self.console_log_handler = cast(
|
console_log_handler = logging.getLogger("django.server").handlers[0]
|
||||||
logging.StreamHandler, logging.getLogger("django.server").handlers[0]
|
assert isinstance(console_log_handler, logging.StreamHandler)
|
||||||
|
assert console_log_handler.stream == stderr
|
||||||
|
|
||||||
|
sys.stderr = console_log_handler.stream = TextIOWrapper(
|
||||||
|
WrappedIO(stderr.buffer, extra_output_finder), line_buffering=True
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
assert isinstance(self.console_log_handler, logging.StreamHandler)
|
yield
|
||||||
assert self.console_log_handler.stream == sys.stderr
|
finally:
|
||||||
self.extra_output_finder = extra_output_finder
|
try:
|
||||||
|
sys.stderr.flush()
|
||||||
def __enter__(self) -> None:
|
finally:
|
||||||
sys.stderr = self # type: ignore[assignment] # Doing tee by swapping stderr stream with custom file like class
|
sys.stderr = console_log_handler.stream = stderr
|
||||||
self.console_log_handler.stream = self
|
|
||||||
|
|
||||||
def __exit__(
|
|
||||||
self,
|
|
||||||
exc_type: Optional[Type[BaseException]],
|
|
||||||
exc_value: Optional[BaseException],
|
|
||||||
traceback: Optional[TracebackType],
|
|
||||||
) -> None:
|
|
||||||
sys.stderr = self.stderr_stream
|
|
||||||
self.console_log_handler.stream = sys.stderr
|
|
||||||
|
|
||||||
def write(self, data: str) -> None:
|
|
||||||
self.stderr_stream.write(data)
|
|
||||||
self.extra_output_finder.find_extra_output(data)
|
|
||||||
|
|
||||||
def writelines(self, data: Sequence[str]) -> None:
|
|
||||||
self.stderr_stream.writelines(data)
|
|
||||||
lines = "".join(data)
|
|
||||||
self.extra_output_finder.find_extra_output(lines)
|
|
||||||
|
|
||||||
def flush(self) -> None:
|
|
||||||
self.stderr_stream.flush()
|
|
||||||
|
|
||||||
|
|
||||||
class TeeStdoutAndFindExtraConsoleOutput:
|
@contextmanager
|
||||||
def __init__(self, extra_output_finder: ExtraConsoleOutputFinder) -> None:
|
def tee_stdout_and_find_extra_console_output(
|
||||||
self.stdout_stream = sys.stdout
|
extra_output_finder: ExtraConsoleOutputFinder,
|
||||||
self.extra_output_finder = extra_output_finder
|
) -> Iterator[None]:
|
||||||
|
stdout = sys.stdout
|
||||||
def __enter__(self) -> None:
|
sys.stdout = TextIOWrapper(
|
||||||
sys.stdout = self # type: ignore[assignment] # Doing tee by swapping stderr stream with custom file like class
|
WrappedIO(sys.stdout.buffer, extra_output_finder), line_buffering=True
|
||||||
|
)
|
||||||
def __exit__(
|
try:
|
||||||
self,
|
yield
|
||||||
exc_type: Optional[Type[BaseException]],
|
finally:
|
||||||
exc_value: Optional[BaseException],
|
try:
|
||||||
traceback: Optional[TracebackType],
|
sys.stdout.flush()
|
||||||
) -> None:
|
finally:
|
||||||
sys.stdout = self.stdout_stream
|
sys.stdout = stdout
|
||||||
|
|
||||||
def write(self, data: str) -> None:
|
|
||||||
self.stdout_stream.write(data)
|
|
||||||
self.extra_output_finder.find_extra_output(data)
|
|
||||||
|
|
||||||
def writelines(self, data: Sequence[str]) -> None:
|
|
||||||
self.stdout_stream.writelines(data)
|
|
||||||
lines = "".join(data)
|
|
||||||
self.extra_output_finder.find_extra_output(lines)
|
|
||||||
|
|
||||||
def flush(self) -> None:
|
|
||||||
self.stdout_stream.flush()
|
|
||||||
|
|||||||
Reference in New Issue
Block a user