| 
									
										
										
										
											2016-09-09 17:37:37 -07:00
										 |  |  | import dis | 
					
						
							|  |  |  | import os.path | 
					
						
							|  |  |  | import re | 
					
						
							|  |  |  | import subprocess | 
					
						
							|  |  |  | import sys | 
					
						
							| 
									
										
										
										
											2023-07-31 11:33:37 +03:00
										 |  |  | import sysconfig | 
					
						
							| 
									
										
										
										
											2016-09-09 17:37:37 -07:00
										 |  |  | import types | 
					
						
							|  |  |  | import unittest | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-03 22:14:31 +02:00
										 |  |  | from test import support | 
					
						
							| 
									
										
										
										
											2021-09-19 15:27:33 +03:00
										 |  |  | from test.support import findfile | 
					
						
							| 
									
										
										
										
											2016-09-09 17:37:37 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-03 22:14:31 +02:00
										 |  |  | if not support.has_subprocess_support: | 
					
						
							|  |  |  |     raise unittest.SkipTest("test module requires subprocess") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-09 17:37:37 -07:00
										 |  |  | def abspath(filename): | 
					
						
							|  |  |  |     return os.path.abspath(findfile(filename, subdir="dtracedata")) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def normalize_trace_output(output): | 
					
						
							|  |  |  |     """Normalize DTrace output for comparison.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     DTrace keeps a per-CPU buffer, and when showing the fired probes, buffers | 
					
						
							|  |  |  |     are concatenated. So if the operating system moves our thread around, the | 
					
						
							|  |  |  |     straight result can be "non-causal". So we add timestamps to the probe | 
					
						
							|  |  |  |     firing, sort by that field, then strip it from the output"""
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # When compiling with '--with-pydebug', strip '[# refs]' debug output. | 
					
						
							|  |  |  |     output = re.sub(r"\[[0-9]+ refs\]", "", output) | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |         result = [ | 
					
						
							|  |  |  |             row.split("\t") | 
					
						
							|  |  |  |             for row in output.splitlines() | 
					
						
							|  |  |  |             if row and not row.startswith('#') | 
					
						
							|  |  |  |         ] | 
					
						
							|  |  |  |         result.sort(key=lambda row: int(row[0])) | 
					
						
							|  |  |  |         result = [row[1] for row in result] | 
					
						
							|  |  |  |         return "\n".join(result) | 
					
						
							|  |  |  |     except (IndexError, ValueError): | 
					
						
							|  |  |  |         raise AssertionError( | 
					
						
							| 
									
										
										
										
											2021-10-07 01:13:48 +02:00
										 |  |  |             "tracer produced unparsable output:\n{}".format(output) | 
					
						
							| 
									
										
										
										
											2016-09-09 17:37:37 -07:00
										 |  |  |         ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class TraceBackend: | 
					
						
							|  |  |  |     EXTENSION = None | 
					
						
							|  |  |  |     COMMAND = None | 
					
						
							|  |  |  |     COMMAND_ARGS = [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def run_case(self, name, optimize_python=None): | 
					
						
							|  |  |  |         actual_output = normalize_trace_output(self.trace_python( | 
					
						
							|  |  |  |             script_file=abspath(name + self.EXTENSION), | 
					
						
							|  |  |  |             python_file=abspath(name + ".py"), | 
					
						
							|  |  |  |             optimize_python=optimize_python)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         with open(abspath(name + self.EXTENSION + ".expected")) as f: | 
					
						
							|  |  |  |             expected_output = f.read().rstrip() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return (expected_output, actual_output) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def generate_trace_command(self, script_file, subcommand=None): | 
					
						
							|  |  |  |         command = self.COMMAND + [script_file] | 
					
						
							|  |  |  |         if subcommand: | 
					
						
							|  |  |  |             command += ["-c", subcommand] | 
					
						
							|  |  |  |         return command | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def trace(self, script_file, subcommand=None): | 
					
						
							|  |  |  |         command = self.generate_trace_command(script_file, subcommand) | 
					
						
							|  |  |  |         stdout, _ = subprocess.Popen(command, | 
					
						
							|  |  |  |                                      stdout=subprocess.PIPE, | 
					
						
							|  |  |  |                                      stderr=subprocess.STDOUT, | 
					
						
							|  |  |  |                                      universal_newlines=True).communicate() | 
					
						
							|  |  |  |         return stdout | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def trace_python(self, script_file, python_file, optimize_python=None): | 
					
						
							|  |  |  |         python_flags = [] | 
					
						
							|  |  |  |         if optimize_python: | 
					
						
							|  |  |  |             python_flags.extend(["-O"] * optimize_python) | 
					
						
							|  |  |  |         subcommand = " ".join([sys.executable] + python_flags + [python_file]) | 
					
						
							|  |  |  |         return self.trace(script_file, subcommand) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def assert_usable(self): | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             output = self.trace(abspath("assert_usable" + self.EXTENSION)) | 
					
						
							|  |  |  |             output = output.strip() | 
					
						
							| 
									
										
										
										
											2018-01-25 19:06:05 +01:00
										 |  |  |         except (FileNotFoundError, NotADirectoryError, PermissionError) as fnfe: | 
					
						
							| 
									
										
										
										
											2016-09-09 17:37:37 -07:00
										 |  |  |             output = str(fnfe) | 
					
						
							|  |  |  |         if output != "probe: success": | 
					
						
							|  |  |  |             raise unittest.SkipTest( | 
					
						
							|  |  |  |                 "{}(1) failed: {}".format(self.COMMAND[0], output) | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class DTraceBackend(TraceBackend): | 
					
						
							|  |  |  |     EXTENSION = ".d" | 
					
						
							|  |  |  |     COMMAND = ["dtrace", "-q", "-s"] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class SystemTapBackend(TraceBackend): | 
					
						
							|  |  |  |     EXTENSION = ".stp" | 
					
						
							|  |  |  |     COMMAND = ["stap", "-g"] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-19 15:27:33 +03:00
										 |  |  | class TraceTests: | 
					
						
							| 
									
										
										
										
											2016-09-09 17:37:37 -07:00
										 |  |  |     # unittest.TestCase options | 
					
						
							|  |  |  |     maxDiff = None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # TraceTests options | 
					
						
							|  |  |  |     backend = None | 
					
						
							|  |  |  |     optimize_python = 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							|  |  |  |     def setUpClass(self): | 
					
						
							|  |  |  |         self.backend.assert_usable() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def run_case(self, name): | 
					
						
							|  |  |  |         actual_output, expected_output = self.backend.run_case( | 
					
						
							|  |  |  |             name, optimize_python=self.optimize_python) | 
					
						
							|  |  |  |         self.assertEqual(actual_output, expected_output) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def test_function_entry_return(self): | 
					
						
							|  |  |  |         self.run_case("call_stack") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def test_verify_call_opcodes(self): | 
					
						
							|  |  |  |         """Ensure our call stack test hits all function call opcodes""" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         opcodes = set(["CALL_FUNCTION", "CALL_FUNCTION_EX", "CALL_FUNCTION_KW"]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         with open(abspath("call_stack.py")) as f: | 
					
						
							|  |  |  |             code_string = f.read() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def get_function_instructions(funcname): | 
					
						
							|  |  |  |             # Recompile with appropriate optimization setting | 
					
						
							|  |  |  |             code = compile(source=code_string, | 
					
						
							|  |  |  |                            filename="<string>", | 
					
						
							|  |  |  |                            mode="exec", | 
					
						
							|  |  |  |                            optimize=self.optimize_python) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             for c in code.co_consts: | 
					
						
							|  |  |  |                 if isinstance(c, types.CodeType) and c.co_name == funcname: | 
					
						
							|  |  |  |                     return dis.get_instructions(c) | 
					
						
							|  |  |  |             return [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         for instruction in get_function_instructions('start'): | 
					
						
							|  |  |  |             opcodes.discard(instruction.opname) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.assertEqual(set(), opcodes) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def test_gc(self): | 
					
						
							|  |  |  |         self.run_case("gc") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def test_line(self): | 
					
						
							|  |  |  |         self.run_case("line") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-19 15:27:33 +03:00
										 |  |  | class DTraceNormalTests(TraceTests, unittest.TestCase): | 
					
						
							| 
									
										
										
										
											2016-09-09 17:37:37 -07:00
										 |  |  |     backend = DTraceBackend() | 
					
						
							|  |  |  |     optimize_python = 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-19 15:27:33 +03:00
										 |  |  | class DTraceOptimizedTests(TraceTests, unittest.TestCase): | 
					
						
							| 
									
										
										
										
											2016-09-09 17:37:37 -07:00
										 |  |  |     backend = DTraceBackend() | 
					
						
							|  |  |  |     optimize_python = 2 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-19 15:27:33 +03:00
										 |  |  | class SystemTapNormalTests(TraceTests, unittest.TestCase): | 
					
						
							| 
									
										
										
										
											2016-09-09 17:37:37 -07:00
										 |  |  |     backend = SystemTapBackend() | 
					
						
							|  |  |  |     optimize_python = 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-19 15:27:33 +03:00
										 |  |  | class SystemTapOptimizedTests(TraceTests, unittest.TestCase): | 
					
						
							| 
									
										
										
										
											2016-09-09 17:37:37 -07:00
										 |  |  |     backend = SystemTapBackend() | 
					
						
							|  |  |  |     optimize_python = 2 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-31 11:33:37 +03:00
										 |  |  | class CheckDtraceProbes(unittest.TestCase): | 
					
						
							|  |  |  |     @classmethod | 
					
						
							|  |  |  |     def setUpClass(cls): | 
					
						
							|  |  |  |         if sysconfig.get_config_var('WITH_DTRACE'): | 
					
						
							|  |  |  |             readelf_major_version, readelf_minor_version = cls.get_readelf_version() | 
					
						
							|  |  |  |             if support.verbose: | 
					
						
							|  |  |  |                 print(f"readelf version: {readelf_major_version}.{readelf_minor_version}") | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             raise unittest.SkipTest("CPython must be configured with the --with-dtrace option.") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @staticmethod | 
					
						
							|  |  |  |     def get_readelf_version(): | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             cmd = ["readelf", "--version"] | 
					
						
							|  |  |  |             proc = subprocess.Popen( | 
					
						
							|  |  |  |                 cmd, | 
					
						
							|  |  |  |                 stdout=subprocess.PIPE, | 
					
						
							|  |  |  |                 stderr=subprocess.PIPE, | 
					
						
							|  |  |  |                 universal_newlines=True, | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |             with proc: | 
					
						
							|  |  |  |                 version, stderr = proc.communicate() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if proc.returncode: | 
					
						
							|  |  |  |                 raise Exception( | 
					
						
							|  |  |  |                     f"Command {' '.join(cmd)!r} failed " | 
					
						
							|  |  |  |                     f"with exit code {proc.returncode}: " | 
					
						
							|  |  |  |                     f"stdout={version!r} stderr={stderr!r}" | 
					
						
							|  |  |  |                 ) | 
					
						
							|  |  |  |         except OSError: | 
					
						
							|  |  |  |             raise unittest.SkipTest("Couldn't find readelf on the path") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Regex to parse: | 
					
						
							|  |  |  |         # 'GNU readelf (GNU Binutils) 2.40.0\n' -> 2.40 | 
					
						
							|  |  |  |         match = re.search(r"^(?:GNU) readelf.*?\b(\d+)\.(\d+)", version) | 
					
						
							|  |  |  |         if match is None: | 
					
						
							|  |  |  |             raise unittest.SkipTest(f"Unable to parse readelf version: {version}") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return int(match.group(1)), int(match.group(2)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_readelf_output(self): | 
					
						
							|  |  |  |         command = ["readelf", "-n", sys.executable] | 
					
						
							|  |  |  |         stdout, _ = subprocess.Popen( | 
					
						
							|  |  |  |             command, | 
					
						
							|  |  |  |             stdout=subprocess.PIPE, | 
					
						
							|  |  |  |             stderr=subprocess.STDOUT, | 
					
						
							|  |  |  |             universal_newlines=True, | 
					
						
							|  |  |  |         ).communicate() | 
					
						
							|  |  |  |         return stdout | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def test_check_probes(self): | 
					
						
							|  |  |  |         readelf_output = self.get_readelf_output() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         available_probe_names = [ | 
					
						
							|  |  |  |             "Name: import__find__load__done", | 
					
						
							|  |  |  |             "Name: import__find__load__start", | 
					
						
							|  |  |  |             "Name: audit", | 
					
						
							|  |  |  |             "Name: gc__start", | 
					
						
							|  |  |  |             "Name: gc__done", | 
					
						
							|  |  |  |         ] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         for probe_name in available_probe_names: | 
					
						
							|  |  |  |             with self.subTest(probe_name=probe_name): | 
					
						
							|  |  |  |                 self.assertIn(probe_name, readelf_output) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @unittest.expectedFailure | 
					
						
							|  |  |  |     def test_missing_probes(self): | 
					
						
							|  |  |  |         readelf_output = self.get_readelf_output() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Missing probes will be added in the future. | 
					
						
							|  |  |  |         missing_probe_names = [ | 
					
						
							|  |  |  |             "Name: function__entry", | 
					
						
							|  |  |  |             "Name: function__return", | 
					
						
							|  |  |  |             "Name: line", | 
					
						
							|  |  |  |         ] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         for probe_name in missing_probe_names: | 
					
						
							|  |  |  |             with self.subTest(probe_name=probe_name): | 
					
						
							|  |  |  |                 self.assertIn(probe_name, readelf_output) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-09 17:37:37 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | if __name__ == '__main__': | 
					
						
							| 
									
										
										
										
											2022-01-18 22:28:18 +03:00
										 |  |  |     unittest.main() |