From 6aca93bcd3c01cddea18c11b4a202b0d5c776da2 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Wed, 24 Apr 2024 14:51:50 +0200 Subject: [PATCH] code: do not truncate args in output when running with -vvv We recently ran into the issue that it would have been useful to get more details from a pytest exception print. With `-vv` one gets all the untruncated local vars. However sometimes it's useful to also get the untruncated arguments. Especially when when working with `subprocess.run()` and `capture_output=True`. We are doing something like: ``` $ cat test/test_foo.py import subprocess def test_foo(): subprocess.run(["sh","-c","seq 400|xargs echo; false"], capture_output=True, text=True, check=True) $ pytest-3 -vv ./test/test_foo.py ============================= test session starts ============================== platform linux -- Python 3.11.8, pytest-7.4.4, pluggy-1.3.0 -- /usr/bin/python3 cachedir: .pytest_cache hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase(PosixPath('/home/mvogt/devel/osbuild/osbuild/.hypothesis/examples')) rootdir: /home/mvogt/devel/osbuild/osbuild plugins: anyio-4.2.0, xdist-3.5.0, hypothesis-6.100.1 collected 1 item test/test_foo.py::test_foo FAILED [100%] =================================== FAILURES =================================== ___________________________________ test_foo ___________________________________ def test_foo(): > subprocess.run(["sh","-c","seq 400|xargs echo; false"], capture_output=True, text=True, check=True) test/test_foo.py:4: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ input = None, capture_output = True, timeout = None, check = True popenargs = (['sh', '-c', 'seq 400|xargs echo; false'],) kwargs = {'stderr': -1, 'stdout': -1, 'text': True} process = stdout = '1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 ... 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400\n' stderr = '', retcode = 1 ``` And some useful information was hidden in the middle of stdout. This commit adds a new `truncate_args` argument similar to the `truncate_locals` in PR#3681 that gets activated with `-vvv` (this is a bit of a strawman, we could add it to `-vv` too or just fold it into `truncate_locals` but it seemed cleaner this way). With the diff the output is now: ``` pytest-3 -vvv ./test/test_foo.py ============================= test session starts ============================== platform linux -- Python 3.11.8, pytest-7.4.4, pluggy-1.3.0 -- /usr/bin/python3 cachedir: .pytest_cache hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase(PosixPath('/home/mvogt/devel/osbuild/osbuild/.hypothesis/examples')) rootdir: /home/mvogt/devel/osbuild/osbuild plugins: anyio-4.2.0, xdist-3.5.0, hypothesis-6.100.1 collected 1 item test/test_foo.py::test_foo FAILED [100%] =================================== FAILURES =================================== ___________________________________ test_foo ___________________________________ def test_foo(): > subprocess.run(["sh","-c","seq 400|xargs echo; false"], capture_output=True, text=True, check=True) test/test_foo.py:4: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ input = None, capture_output = True, timeout = None, check = True popenargs = (['sh', '-c', 'seq 400|xargs echo; false'],) kwargs = {'stderr': -1, 'stdout': -1, 'text': True} process = stdout = ('1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 ' '29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 ' '54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 ' '79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 ' '103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 ' '122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 ' '141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 ' '160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 ' '179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 ' '198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 ' '217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 ' '236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 ' '255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 ' '274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 ' '293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 ' '312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 ' '331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 ' '350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 ' '369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 ' '388 389 390 391 392 393 394 395 396 397 398 399 400\n') stderr = '', retcode = 1 ``` --- AUTHORS | 1 + src/_pytest/_code/code.py | 12 +++++++++++- src/_pytest/nodes.py | 6 ++++++ testing/code/test_excinfo.py | 22 ++++++++++++++++++++++ 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 7c35a6152..753f1a085 100644 --- a/AUTHORS +++ b/AUTHORS @@ -276,6 +276,7 @@ Michael Droettboom Michael Goerz Michael Krebs Michael Seifert +Michael Vogt Michal Wajszczuk Michał Górny Michał Zięba diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 3b4a62a4f..27678df96 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -636,6 +636,7 @@ class ExceptionInfo(Generic[E]): ] = True, funcargs: bool = False, truncate_locals: bool = True, + truncate_args: bool = True, chain: bool = True, ) -> Union["ReprExceptionInfo", "ExceptionChainRepr"]: """Return str()able representation of this exception info. @@ -666,6 +667,9 @@ class ExceptionInfo(Generic[E]): :param bool truncate_locals: With ``showlocals==True``, make sure locals can be safely represented as strings. + :param bool truncate_args: + With ``showargs==True``, make sure args can be safely represented as strings. + :param bool chain: If chained exceptions in Python 3 should be shown. @@ -692,6 +696,7 @@ class ExceptionInfo(Generic[E]): tbfilter=tbfilter, funcargs=funcargs, truncate_locals=truncate_locals, + truncate_args=truncate_args, chain=chain, ) return fmt.repr_excinfo(self) @@ -810,6 +815,7 @@ class FormattedExcinfo: tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True funcargs: bool = False truncate_locals: bool = True + truncate_args: bool = True chain: bool = True astcache: Dict[Union[str, Path], ast.AST] = dataclasses.field( default_factory=dict, init=False, repr=False @@ -840,7 +846,11 @@ class FormattedExcinfo: if self.funcargs: args = [] for argname, argvalue in entry.frame.getargs(var=True): - args.append((argname, saferepr(argvalue))) + if self.truncate_args: + str_repr = saferepr(argvalue) + else: + str_repr = safeformat(argvalue) + args.append((argname, str_repr)) return ReprFuncArgs(args) return None diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 1b91bdb6e..9e25562b7 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -448,6 +448,11 @@ class Node(abc.ABC, metaclass=NodeMeta): else: truncate_locals = True + if self.config.getoption("verbose", 0) > 2: + truncate_args = False + else: + truncate_args = True + # excinfo.getrepr() formats paths relative to the CWD if `abspath` is False. # It is possible for a fixture/test to change the CWD while this code runs, which # would then result in the user seeing confusing paths in the failure message. @@ -466,6 +471,7 @@ class Node(abc.ABC, metaclass=NodeMeta): style=style, tbfilter=tbfilter, truncate_locals=truncate_locals, + truncate_args=truncate_args, ) def repr_failure( diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 419c11abc..fefb3cdf2 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -708,6 +708,28 @@ raise ValueError() assert full_reprlocals.lines assert full_reprlocals.lines[0] == "l = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]" + def test_repr_args_not_truncated(self, importasmod) -> None: + mod = importasmod( + """ + def func1(m): + raise ValueError("hello\\nworld") + """ + ) + excinfo = pytest.raises(ValueError, mod.func1, "m" * 500) + excinfo.traceback = excinfo.traceback.filter(excinfo) + entry = excinfo.traceback[-1] + p = FormattedExcinfo(funcargs=True, truncate_args=True) + reprfuncargs = p.repr_args(entry) + assert reprfuncargs is not None + assert len(reprfuncargs.args[0][1]) < 500 + assert "..." in reprfuncargs.args[0][1] + # again without truncate + p = FormattedExcinfo(funcargs=True, truncate_args=False) + reprfuncargs = p.repr_args(entry) + assert reprfuncargs is not None + assert reprfuncargs.args[0] == ("m", repr("m" * 500)) + assert "..." not in reprfuncargs.args[0][1] + def test_repr_tracebackentry_lines(self, importasmod) -> None: mod = importasmod( """