From bd6af2cfc8791f80b06f2089b5332dd0df233917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 9 Dec 2024 12:26:45 +0100 Subject: [PATCH 1/5] add tests for `dis` command-line interface --- Lib/dis.py | 4 +- Lib/test/test_dis.py | 104 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 104 insertions(+), 4 deletions(-) diff --git a/Lib/dis.py b/Lib/dis.py index 6b3e9ef8399e1c..aa22404c6687e1 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -1115,7 +1115,7 @@ def dis(self): return output.getvalue() -def main(): +def main(args=None): import argparse parser = argparse.ArgumentParser() @@ -1128,7 +1128,7 @@ def main(): parser.add_argument('-S', '--specialized', action='store_true', help='show specialized bytecode') parser.add_argument('infile', nargs='?', default='-') - args = parser.parse_args() + args = parser.parse_args(args=args) if args.infile == '-': name = '' source = sys.stdin.buffer.read() diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 55890e58ed4bae..21da55bc0f3f7c 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -5,15 +5,19 @@ import dis import functools import io +import itertools +import opcode import re import sys +import tempfile +import textwrap import types import unittest from test.support import (captured_stdout, requires_debug_ranges, - requires_specialization, cpython_only) + requires_specialization, cpython_only, + os_helper) from test.support.bytecode_helper import BytecodeTestCase -import opcode CACHE = dis.opmap["CACHE"] @@ -2426,5 +2430,101 @@ def _unroll_caches_as_Instructions(instrs, show_caches=False): False, None, None, instr.positions) +class TestDisCLI(unittest.TestCase): + + def infile(self, content): + filename = tempfile.mktemp() + self.addCleanup(os_helper.unlink, filename) + with open(filename, 'w') as fp: + fp.write(content) + return filename + + def invoke_dis(self, infile, *flags): + output = io.StringIO() + with contextlib.redirect_stdout(output): + dis.main(args=[*flags, infile]) + return output.getvalue() + + def check_output(self, source, expect, *flags): + with self.subTest(flags): + infile = self.infile(source) + res = self.invoke_dis(infile, *flags) + res = textwrap.dedent(res) + expect = textwrap.dedent(expect) + self.assertListEqual(res.splitlines(), expect.splitlines()) + + def test_invokation(self): + # test various combinations of parameters + base_flags = [ + ('-C', '--show-caches'), + ('-O', '--show-offsets'), + ('-P', '--show-positions'), + ('-S', '--specialized'), + ] + + infile = self.infile('def f():\n\tprint(x)\n\treturn None') + + for r in range(1, len(base_flags) + 1): + for choices in itertools.combinations(base_flags, r=r): + for args in itertools.product(*choices): + with self.subTest(args=args[1:]): + _ = self.invoke_dis(infile, *args) + + def test_show_cache(self): + # test 'python -m dis -C/--show-caches' + source = 'print()' + expect = '''\ +0 RESUME 0 + +1 LOAD_NAME 0 (print) + PUSH_NULL + CALL 0 + CACHE 0 (counter: 0) + CACHE 0 (func_version: 0) + CACHE 0 + POP_TOP + LOAD_CONST 0 (None) + RETURN_VALUE +''' + for flag in ['-C', '--show-caches']: + self.check_output(source, expect, flag) + + def test_show_offsets(self): + # test 'python -m dis -O/--show-offsets' + source = 'pass' + expect = '''\ +0 0 RESUME 0 + +1 2 LOAD_CONST 0 (None) + 4 RETURN_VALUE + ''' + for flag in ['-O', '--show-offsets']: + self.check_output(source, expect, flag) + + def test_show_positions(self): + # test 'python -m dis -P/--show-positions' + source = 'pass' + expect = '''\ +0:0-1:0 RESUME 0 + +1:0-1:4 LOAD_CONST 0 (None) +1:0-1:4 RETURN_VALUE +''' + for flag in ['-P', '--show-positions']: + self.check_output(source, expect, flag) + + def test_specialized_code(self): + # test 'python -m dis -S/--specialized' + source = 'pass' + expect = '''\ +0 RESUME 0 + +1 LOAD_CONST_IMMORTAL 0 (None) + RETURN_VALUE +''' + for flag in ['-S', '--specialized']: + self.check_output(source, expect, flag) + + if __name__ == "__main__": unittest.main() From a240b8134e895ecca7017158509dc4a44be4bebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 9 Dec 2024 12:35:52 +0100 Subject: [PATCH 2/5] blurb --- .../next/Tests/2024-12-09-12-35-44.gh-issue-127637.KLx-9I.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Tests/2024-12-09-12-35-44.gh-issue-127637.KLx-9I.rst diff --git a/Misc/NEWS.d/next/Tests/2024-12-09-12-35-44.gh-issue-127637.KLx-9I.rst b/Misc/NEWS.d/next/Tests/2024-12-09-12-35-44.gh-issue-127637.KLx-9I.rst new file mode 100644 index 00000000000000..ac5d9827b07199 --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2024-12-09-12-35-44.gh-issue-127637.KLx-9I.rst @@ -0,0 +1 @@ +Add tests for the :mod:`dis` command-line interface. Patch by Bénédikt Tran. From d27781492ac7ca2f3207a32049457535bf190aa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:01:08 +0100 Subject: [PATCH 3/5] address Irit's review --- Lib/test/test_dis.py | 88 +++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 42 deletions(-) diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 21da55bc0f3f7c..c68c4255e10ce9 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -2432,25 +2432,25 @@ def _unroll_caches_as_Instructions(instrs, show_caches=False): class TestDisCLI(unittest.TestCase): - def infile(self, content): - filename = tempfile.mktemp() - self.addCleanup(os_helper.unlink, filename) - with open(filename, 'w') as fp: - fp.write(content) - return filename - - def invoke_dis(self, infile, *flags): + def setUp(self): + self.filename = tempfile.mktemp() + self.addCleanup(os_helper.unlink, self.filename) + + def set_source(self, content): + with open(self.filename, 'w') as fp: + fp.write(textwrap.dedent(content).strip()) + + def invoke_dis(self, *flags): output = io.StringIO() with contextlib.redirect_stdout(output): - dis.main(args=[*flags, infile]) + dis.main(args=[*flags, self.filename]) return output.getvalue() def check_output(self, source, expect, *flags): with self.subTest(flags): - infile = self.infile(source) - res = self.invoke_dis(infile, *flags) + self.set_source(source) + res = self.invoke_dis(*flags) res = textwrap.dedent(res) - expect = textwrap.dedent(expect) self.assertListEqual(res.splitlines(), expect.splitlines()) def test_invokation(self): @@ -2462,66 +2462,70 @@ def test_invokation(self): ('-S', '--specialized'), ] - infile = self.infile('def f():\n\tprint(x)\n\treturn None') + self.set_source(''' + def f(): + print(x) + return None + ''') for r in range(1, len(base_flags) + 1): for choices in itertools.combinations(base_flags, r=r): for args in itertools.product(*choices): with self.subTest(args=args[1:]): - _ = self.invoke_dis(infile, *args) + _ = self.invoke_dis(*args) def test_show_cache(self): # test 'python -m dis -C/--show-caches' source = 'print()' - expect = '''\ -0 RESUME 0 - -1 LOAD_NAME 0 (print) - PUSH_NULL - CALL 0 - CACHE 0 (counter: 0) - CACHE 0 (func_version: 0) - CACHE 0 - POP_TOP - LOAD_CONST 0 (None) - RETURN_VALUE -''' + expect = textwrap.dedent(''' + 0 RESUME 0 + + 1 LOAD_NAME 0 (print) + PUSH_NULL + CALL 0 + CACHE 0 (counter: 0) + CACHE 0 (func_version: 0) + CACHE 0 + POP_TOP + LOAD_CONST 0 (None) + RETURN_VALUE + ''').strip() for flag in ['-C', '--show-caches']: self.check_output(source, expect, flag) def test_show_offsets(self): # test 'python -m dis -O/--show-offsets' source = 'pass' - expect = '''\ -0 0 RESUME 0 + expect = textwrap.dedent(''' + 0 0 RESUME 0 -1 2 LOAD_CONST 0 (None) - 4 RETURN_VALUE - ''' + 1 2 LOAD_CONST 0 (None) + 4 RETURN_VALUE + ''').strip() for flag in ['-O', '--show-offsets']: self.check_output(source, expect, flag) def test_show_positions(self): # test 'python -m dis -P/--show-positions' source = 'pass' - expect = '''\ -0:0-1:0 RESUME 0 + expect = textwrap.dedent(''' + 0:0-1:0 RESUME 0 -1:0-1:4 LOAD_CONST 0 (None) -1:0-1:4 RETURN_VALUE -''' + 1:0-1:4 LOAD_CONST 0 (None) + 1:0-1:4 RETURN_VALUE + ''').strip() for flag in ['-P', '--show-positions']: self.check_output(source, expect, flag) def test_specialized_code(self): # test 'python -m dis -S/--specialized' source = 'pass' - expect = '''\ -0 RESUME 0 + expect = textwrap.dedent(''' + 0 RESUME 0 -1 LOAD_CONST_IMMORTAL 0 (None) - RETURN_VALUE -''' + 1 LOAD_CONST_IMMORTAL 0 (None) + RETURN_VALUE + ''').strip() for flag in ['-S', '--specialized']: self.check_output(source, expect, flag) From c2a0b43ad30b951810969d8f6dbeb0861563ca2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:06:47 +0100 Subject: [PATCH 4/5] address Irit's review (round 2) --- Lib/test/test_dis.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index c68c4255e10ce9..f5a99a173dbe21 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -2436,21 +2436,30 @@ def setUp(self): self.filename = tempfile.mktemp() self.addCleanup(os_helper.unlink, self.filename) + @staticmethod + def text_normalize(string): + """Dedent *string* and strip it from its surrounding whitespaces. + + This method is used by the other utility functions so that any + string to write or to match against can be freely indented. + """ + return textwrap.dedent(string).strip() + def set_source(self, content): with open(self.filename, 'w') as fp: - fp.write(textwrap.dedent(content).strip()) + fp.write(self.text_normalize(content)) def invoke_dis(self, *flags): output = io.StringIO() with contextlib.redirect_stdout(output): dis.main(args=[*flags, self.filename]) - return output.getvalue() + return self.text_normalize(output.getvalue()) def check_output(self, source, expect, *flags): - with self.subTest(flags): + with self.subTest(source=source, flags=flags): self.set_source(source) res = self.invoke_dis(*flags) - res = textwrap.dedent(res) + expect = self.text_normalize(expect) self.assertListEqual(res.splitlines(), expect.splitlines()) def test_invokation(self): @@ -2477,7 +2486,7 @@ def f(): def test_show_cache(self): # test 'python -m dis -C/--show-caches' source = 'print()' - expect = textwrap.dedent(''' + expect = ''' 0 RESUME 0 1 LOAD_NAME 0 (print) @@ -2489,43 +2498,43 @@ def test_show_cache(self): POP_TOP LOAD_CONST 0 (None) RETURN_VALUE - ''').strip() + ''' for flag in ['-C', '--show-caches']: self.check_output(source, expect, flag) def test_show_offsets(self): # test 'python -m dis -O/--show-offsets' source = 'pass' - expect = textwrap.dedent(''' + expect = ''' 0 0 RESUME 0 1 2 LOAD_CONST 0 (None) 4 RETURN_VALUE - ''').strip() + ''' for flag in ['-O', '--show-offsets']: self.check_output(source, expect, flag) def test_show_positions(self): # test 'python -m dis -P/--show-positions' source = 'pass' - expect = textwrap.dedent(''' + expect = ''' 0:0-1:0 RESUME 0 1:0-1:4 LOAD_CONST 0 (None) 1:0-1:4 RETURN_VALUE - ''').strip() + ''' for flag in ['-P', '--show-positions']: self.check_output(source, expect, flag) def test_specialized_code(self): # test 'python -m dis -S/--specialized' source = 'pass' - expect = textwrap.dedent(''' + expect = ''' 0 RESUME 0 1 LOAD_CONST_IMMORTAL 0 (None) RETURN_VALUE - ''').strip() + ''' for flag in ['-S', '--specialized']: self.check_output(source, expect, flag) From ca68f3bda423bc08cb54657701a393857b9b3a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:15:41 +0100 Subject: [PATCH 5/5] add test for unknown option --- Lib/test/test_dis.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index f5a99a173dbe21..c719f571152d61 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -2462,7 +2462,7 @@ def check_output(self, source, expect, *flags): expect = self.text_normalize(expect) self.assertListEqual(res.splitlines(), expect.splitlines()) - def test_invokation(self): + def test_invocation(self): # test various combinations of parameters base_flags = [ ('-C', '--show-caches'), @@ -2483,6 +2483,11 @@ def f(): with self.subTest(args=args[1:]): _ = self.invoke_dis(*args) + with self.assertRaises(SystemExit): + # suppress argparse error message + with contextlib.redirect_stderr(io.StringIO()): + _ = self.invoke_dis('--unknown') + def test_show_cache(self): # test 'python -m dis -C/--show-caches' source = 'print()'