diff --git a/pythonFiles/testing_tools/adapter/pytest/_discovery.py b/pythonFiles/testing_tools/adapter/pytest/_discovery.py index 5efac3a388ec..aee1a1eccb98 100644 --- a/pythonFiles/testing_tools/adapter/pytest/_discovery.py +++ b/pythonFiles/testing_tools/adapter/pytest/_discovery.py @@ -28,11 +28,15 @@ def discover(pytestargs=None, hidestdio=False, # No tests were discovered. pass elif ec != 0: + print(('equivalent command: {} -m pytest {}' + ).format(sys.executable, util.shlex_unsplit(pytestargs))) if hidestdio: print(stdio.getvalue(), file=sys.stderr) sys.stdout.flush() raise Exception('pytest discovery failed (exit code {})'.format(ec)) if not _plugin._started: + print(('equivalent command: {} -m pytest {}' + ).format(sys.executable, util.shlex_unsplit(pytestargs))) if hidestdio: print(stdio.getvalue(), file=sys.stderr) sys.stdout.flush() diff --git a/pythonFiles/testing_tools/adapter/util.py b/pythonFiles/testing_tools/adapter/util.py index 464fc469e8d9..d8df4eb25485 100644 --- a/pythonFiles/testing_tools/adapter/util.py +++ b/pythonFiles/testing_tools/adapter/util.py @@ -59,3 +59,34 @@ def group_attr_names(attrnames): group = 'other' grouped[group].append(name) return grouped + + +def shlex_unsplit(argv): + """Return the shell-safe string for the given arguments. + + This effectively the equivalent of reversing shlex.split(). + """ + argv = [_quote_arg(a) for a in argv] + return ' '.join(argv) + + +try: + from shlex import quote as _quote_arg +except ImportError: + def _quote_arg(arg): + parts = None + for i, c in enumerate(arg): + if c.isspace(): + pass + elif c == '"': + pass + elif c == "'": + c = "'\"'\"'" + else: + continue + if parts is None: + parts = list(arg) + parts[i] = c + if parts is not None: + arg = "'" + ''.join(parts) + "'" + return arg diff --git a/pythonFiles/tests/testing_tools/adapter/test_util.py b/pythonFiles/tests/testing_tools/adapter/test_util.py new file mode 100644 index 000000000000..eabca9cdd475 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/test_util.py @@ -0,0 +1,59 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import shlex +import unittest + +from testing_tools.adapter.util import shlex_unsplit + + +class ShlexUnsplitTests(unittest.TestCase): + + def test_no_args(self): + argv = [] + joined = shlex_unsplit(argv) + + self.assertEqual(joined, '') + self.assertEqual(shlex.split(joined), argv) + + def test_one_arg(self): + argv = ['spam'] + joined = shlex_unsplit(argv) + + self.assertEqual(joined, 'spam') + self.assertEqual(shlex.split(joined), argv) + + def test_multiple_args(self): + argv = [ + '-x', 'X', + '-xyz', + 'spam', + 'eggs', + ] + joined = shlex_unsplit(argv) + + self.assertEqual(joined, '-x X -xyz spam eggs') + self.assertEqual(shlex.split(joined), argv) + + def test_whitespace(self): + argv = [ + '-x', 'X Y Z', + 'spam spam\tspam', + 'eggs', + ] + joined = shlex_unsplit(argv) + + self.assertEqual(joined, "-x 'X Y Z' 'spam spam\tspam' eggs") + self.assertEqual(shlex.split(joined), argv) + + def test_quotation_marks(self): + argv = [ + '-x', "''", + 'spam"spam"spam', + "ham'ham'ham", + 'eggs', + ] + joined = shlex_unsplit(argv) + + self.assertEqual(joined, "-x ''\"'\"''\"'\"'' 'spam\"spam\"spam' 'ham'\"'\"'ham'\"'\"'ham' eggs") + self.assertEqual(shlex.split(joined), argv)