From 6aca93bcd3c01cddea18c11b4a202b0d5c776da2 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Wed, 24 Apr 2024 14:51:50 +0200 Subject: [PATCH 1/7] 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 7c35a615232..753f1a085a0 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 3b4a62a4fa0..27678df96be 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -636,6 +636,7 @@ def getrepr( ] = 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 @@ def getrepr( :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 @@ def getrepr( 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 @@ def repr_args(self, entry: TracebackEntry) -> Optional["ReprFuncArgs"]: 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 1b91bdb6e43..9e25562b7fe 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -448,6 +448,11 @@ def _repr_failure_py( 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 @@ def _repr_failure_py( 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 419c11abcc0..fefb3cdf2d2 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -708,6 +708,28 @@ def test_repr_local_truncated(self) -> None: 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( """ From fa769e26127bb03eb3595cf289e3b330c3ec791e Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Mon, 3 Jun 2024 08:52:38 +0200 Subject: [PATCH 2/7] _pytest: address review feedback (thanks Bruno!) --- src/_pytest/_code/code.py | 2 +- src/_pytest/nodes.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 27678df96be..95fac715a5a 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -849,7 +849,7 @@ def repr_args(self, entry: TracebackEntry) -> Optional["ReprFuncArgs"]: if self.truncate_args: str_repr = saferepr(argvalue) else: - str_repr = safeformat(argvalue) + str_repr = saferepr(argvalue, maxsize=None) args.append((argname, str_repr)) return ReprFuncArgs(args) return None diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 9e25562b7fe..4a974855c6f 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -448,10 +448,7 @@ def _repr_failure_py( else: truncate_locals = True - if self.config.getoption("verbose", 0) > 2: - truncate_args = False - else: - truncate_args = True + truncate_args = False if self.config.getoption("verbose", 0) > 2 else 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 From 85c90c5a4377db544e058f77039dda0799f145cb Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Mon, 3 Jun 2024 09:54:54 +0200 Subject: [PATCH 3/7] testing: add test_full_output_vvv() integration test This commit adds an integration test for the `-vvv` feature. Thanks to Bruno Oliveira for helping me how to write it! --- testing/test_assertion.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index ef4e36644d9..07807daa1c9 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -2045,3 +2045,36 @@ def test_long_text_fail(): f"E AssertionError: assert 'hello world' in '{long_text}'", ] ) + + +def test_full_output_vvv(pytester: Pytester) -> None: + pytester.makepyfile( + r""" + def crash_helper(m): + assert 1 == 2 + def test_vvv(): + crash_helper(500 * "a") + """ + ) + result = pytester.runpytest("") + # without -vvv, the passed args are truncated + expected_non_vvv_arg_line = "m = 'aaaaaaaaaaaaaaa*..aaaaaaaaaaaa*" + result.stdout.fnmatch_lines( + [ + expected_non_vvv_arg_line, + "test_full_output_vvv.py:2: AssertionError", + ], + ) + # double check that the untruncated part is not in the output + expected_vvv_arg_line = "m = '{}'".format(500 * "a") + result.stdout.no_fnmatch_line(expected_vvv_arg_line) + + # but with "-vvv" the args are not truncated + result = pytester.runpytest("-vvv") + result.stdout.fnmatch_lines( + [ + expected_vvv_arg_line, + "test_full_output_vvv.py:2: AssertionError", + ] + ) + result.stdout.no_fnmatch_line(expected_non_vvv_arg_line) From 9b69bba347df0cfe78a0f2d1eb32481d17150c71 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Mon, 3 Jun 2024 09:55:08 +0200 Subject: [PATCH 4/7] changelog: add changelog entry for new `-vvv` feature --- changelog/2871.improvement.rst | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 changelog/2871.improvement.rst diff --git a/changelog/2871.improvement.rst b/changelog/2871.improvement.rst new file mode 100644 index 00000000000..4f2d4118925 --- /dev/null +++ b/changelog/2871.improvement.rst @@ -0,0 +1,31 @@ +Do not truncate the args in output when running with `-vvv` + +Sometimes it is useful to get the untruncated arguments as part of the +pytest output. This can be useful when using e.g. `subprocess.run()` +and `capture_output=True`. When passing `-vvv` the full content of +`stdout` is then available as part of the `pytest -vvv` output, e.g. + +```python +def test_vvv(): + subprocess.run(["sh","-c","seq 400|xargs echo; false"], capture_output=True, text=True, encoding="utf8", check=True) +``` + +will now output: +``` +$ pytest -vvv +=================================== FAILURES =================================== +___________________________________ test_vvv ___________________________________ + + def test_vvv(): +> subprocess.run(["sh","-c","seq 400|xargs echo; false"], capture_output=True, text=True, encoding="utf8", check=True) + +test_full_output_vvv.py:3: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +input = None, capture_output = True, timeout = None, check = True +popenargs = (['sh', '-c', 'seq 400|xargs echo; false'],) +kwargs = {'encoding': 'utf8', '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' +... +``` From 1f19489e128e38abb1430301111ef24a5b28caac Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 07:58:43 +0000 Subject: [PATCH 5/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- changelog/2871.improvement.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/changelog/2871.improvement.rst b/changelog/2871.improvement.rst index 4f2d4118925..e6936c0ec70 100644 --- a/changelog/2871.improvement.rst +++ b/changelog/2871.improvement.rst @@ -7,7 +7,13 @@ and `capture_output=True`. When passing `-vvv` the full content of ```python def test_vvv(): - subprocess.run(["sh","-c","seq 400|xargs echo; false"], capture_output=True, text=True, encoding="utf8", check=True) + subprocess.run( + ["sh", "-c", "seq 400|xargs echo; false"], + capture_output=True, + text=True, + encoding="utf8", + check=True, + ) ``` will now output: From d4ed4bf02c0589a35303edeecf0f920997947bf2 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Mon, 3 Jun 2024 10:22:57 +0200 Subject: [PATCH 6/7] testing: fix mypy issues in test_repr_args_not_truncated --- testing/code/test_excinfo.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index fefb3cdf2d2..ddd937a0882 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -10,6 +10,7 @@ import sys import textwrap from typing import Any +from typing import cast from typing import TYPE_CHECKING import _pytest._code @@ -721,14 +722,15 @@ def func1(m): 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] + arg1 = cast(str, reprfuncargs.args[0][1]) + assert len(arg1) < 500 + assert "..." in arg1 # 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] + assert "..." not in cast(str, reprfuncargs.args[0][1]) def test_repr_tracebackentry_lines(self, importasmod) -> None: mod = importasmod( From fd1d2defa88e74fd5c8814dc4bafa5b230c7057e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 3 Jun 2024 08:57:40 -0300 Subject: [PATCH 7/7] Update changelog/2871.improvement.rst --- changelog/2871.improvement.rst | 38 +--------------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/changelog/2871.improvement.rst b/changelog/2871.improvement.rst index e6936c0ec70..1ba399550c7 100644 --- a/changelog/2871.improvement.rst +++ b/changelog/2871.improvement.rst @@ -1,37 +1 @@ -Do not truncate the args in output when running with `-vvv` - -Sometimes it is useful to get the untruncated arguments as part of the -pytest output. This can be useful when using e.g. `subprocess.run()` -and `capture_output=True`. When passing `-vvv` the full content of -`stdout` is then available as part of the `pytest -vvv` output, e.g. - -```python -def test_vvv(): - subprocess.run( - ["sh", "-c", "seq 400|xargs echo; false"], - capture_output=True, - text=True, - encoding="utf8", - check=True, - ) -``` - -will now output: -``` -$ pytest -vvv -=================================== FAILURES =================================== -___________________________________ test_vvv ___________________________________ - - def test_vvv(): -> subprocess.run(["sh","-c","seq 400|xargs echo; false"], capture_output=True, text=True, encoding="utf8", check=True) - -test_full_output_vvv.py:3: -_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - -input = None, capture_output = True, timeout = None, check = True -popenargs = (['sh', '-c', 'seq 400|xargs echo; false'],) -kwargs = {'encoding': 'utf8', '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' -... -``` +Do not truncate arguments to functions in output when running with `-vvv`.