-
-
Notifications
You must be signed in to change notification settings - Fork 32.5k
GH-135904: Add tests for the JIT build process #136766
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7a6d819
a322ad4
e1eb85d
f4c05b3
00cd7e3
202fb6f
4e5554c
9db0563
c4c3ccc
c6fc7bd
558c83b
867a686
7c9c3ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import asyncio | ||
import pathlib | ||
import shlex | ||
import sysconfig | ||
import tempfile | ||
import test.support | ||
import test.test_tools | ||
import test.support.script_helper | ||
import unittest | ||
|
||
_CPYTHON = pathlib.Path(test.support.REPO_ROOT).resolve() | ||
_TOOLS_JIT = _CPYTHON / "Tools" / "jit" | ||
_TOOLS_JIT_TEST = _TOOLS_JIT / "test" | ||
_TOOLS_JIT_TEST_TEST_EXECUTOR_CASES_C_H = _TOOLS_JIT_TEST / "test_executor_cases.c.h" | ||
_TOOLS_JIT_BUILD_PY = _TOOLS_JIT / "build.py" | ||
|
||
# Skip this test if either the JIT build scripts or the needed LLVM utilities | ||
# are missing: | ||
test.test_tools.skip_if_missing("jit") | ||
with test.test_tools.imports_under_tool("jit"): | ||
import _llvm | ||
for tool in ["clang", "llvm-objdump", "llvm-readobj"]: | ||
if not asyncio.run(_llvm._find_tool(tool)): | ||
raise unittest.SkipTest(f"{tool} {_llvm._LLVM_VERSION} isn't installed.") | ||
|
||
@test.support.cpython_only | ||
@unittest.skipIf(test.support.Py_DEBUG, "Debug stencils aren't tested.") | ||
@unittest.skipIf(test.support.Py_GIL_DISABLED, "Free-threaded stencils aren't tested.") | ||
class TestJITStencils(unittest.TestCase): | ||
|
||
def _build_jit_stencils(self, target: str) -> str: | ||
with tempfile.TemporaryDirectory() as work: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, I'll take a look! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it, like, better? Haha. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Couldn't tell you 😅 but it seems quite common. Though both are used so it probably doesn't matter :) |
||
jit_stencils_h = pathlib.Path(work, f"jit_stencils-{target}.h").resolve() | ||
pyconfig_h = pathlib.Path(sysconfig.get_config_h_filename()).resolve() | ||
result, args = test.support.script_helper.run_python_until_end( | ||
_TOOLS_JIT_BUILD_PY, | ||
"--input-file", _TOOLS_JIT_TEST_TEST_EXECUTOR_CASES_C_H, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is just for my understanding. is this needed in case we need to regenerate this during development and we need to test it, right? |
||
"--output-dir", jit_stencils_h.parent, | ||
"--pyconfig-dir", pyconfig_h.parent, | ||
target, | ||
__isolated=False, | ||
# Windows leaks temporary files on failure because the JIT build | ||
# process is async. This forces it to be "sync" for this test: | ||
PYTHON_CPU_COUNT="1", | ||
) | ||
if result.rc: | ||
self.skipTest(f"Build failed: {shlex.join(map(str, args))}") | ||
body = jit_stencils_h.read_text() | ||
# Strip out two lines of header comments: | ||
_, _, body = body.split("\n", 2) | ||
return body | ||
|
||
def _check_jit_stencils( | ||
self, expected: str, actual: str, test_jit_stencils_h: pathlib.Path | ||
) -> None: | ||
try: | ||
self.assertEqual(expected.strip("\n"), actual.strip("\n")) | ||
except AssertionError as e: | ||
# Make it easy to re-validate the expected output: | ||
relative = test_jit_stencils_h.relative_to(_CPYTHON) | ||
message = f"If this is expected, replace {relative} with:" | ||
banner = "=" * len(message) | ||
e.add_note("\n".join([banner, message, banner])) | ||
e.add_note(actual) | ||
raise | ||
|
||
def test_jit_stencils(self): | ||
self.maxDiff = None | ||
found = False | ||
for test_jit_stencils_h in _TOOLS_JIT_TEST.glob("test_jit_stencils-*.h"): | ||
target = test_jit_stencils_h.stem.removeprefix("test_jit_stencils-") | ||
with self.subTest(target): | ||
expected = test_jit_stencils_h.read_text() | ||
actual = self._build_jit_stencils(target) | ||
found = True | ||
self._check_jit_stencils(expected, actual, test_jit_stencils_h) | ||
self.assertTrue(found, "No JIT stencils built!") | ||
|
||
|
||
if __name__ == "__main__": | ||
unittest.main() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,12 +19,16 @@ class HoleValue(enum.Enum): | |
CODE = enum.auto() | ||
# The base address of the read-only data for this uop: | ||
DATA = enum.auto() | ||
# The base address of the machine code for the error jump target (exposed as _JIT_ERROR_TARGET): | ||
ERROR_TARGET = enum.auto() | ||
# The address of the current executor (exposed as _JIT_EXECUTOR): | ||
EXECUTOR = enum.auto() | ||
# The base address of the "global" offset table located in the read-only data. | ||
# Shouldn't be present in the final stencils, since these are all replaced with | ||
# equivalent DATA values: | ||
GOT = enum.auto() | ||
# The base address of the machine code for the jump target (exposed as _JIT_JUMP_TARGET): | ||
JUMP_TARGET = enum.auto() | ||
# The current uop's oparg (exposed as _JIT_OPARG): | ||
OPARG = enum.auto() | ||
# The current uop's operand0 on 64-bit platforms (exposed as _JIT_OPERAND0): | ||
|
@@ -39,10 +43,9 @@ class HoleValue(enum.Enum): | |
OPERAND1_LO = enum.auto() | ||
# The current uop's target (exposed as _JIT_TARGET): | ||
TARGET = enum.auto() | ||
# The base address of the machine code for the jump target (exposed as _JIT_JUMP_TARGET): | ||
JUMP_TARGET = enum.auto() | ||
# The base address of the machine code for the error jump target (exposed as _JIT_ERROR_TARGET): | ||
ERROR_TARGET = enum.auto() | ||
# Writable data, which we don't support! Optimistically remove their data | ||
# from the stencil, and raise later if they're actually used: | ||
WRITABLE = enum.auto() | ||
# A hardcoded value of zero (used for symbol lookups): | ||
ZERO = enum.auto() | ||
|
||
|
@@ -96,9 +99,11 @@ class HoleValue(enum.Enum): | |
_HOLE_EXPRS = { | ||
HoleValue.CODE: "(uintptr_t)code", | ||
HoleValue.DATA: "(uintptr_t)data", | ||
HoleValue.ERROR_TARGET: "state->instruction_starts[instruction->error_target]", | ||
HoleValue.EXECUTOR: "(uintptr_t)executor", | ||
# These should all have been turned into DATA values by process_relocations: | ||
# HoleValue.GOT: "", | ||
HoleValue.JUMP_TARGET: "state->instruction_starts[instruction->jump_target]", | ||
HoleValue.OPARG: "instruction->oparg", | ||
HoleValue.OPERAND0: "instruction->operand0", | ||
HoleValue.OPERAND0_HI: "(instruction->operand0 >> 32)", | ||
|
@@ -107,8 +112,8 @@ class HoleValue(enum.Enum): | |
HoleValue.OPERAND1_HI: "(instruction->operand1 >> 32)", | ||
HoleValue.OPERAND1_LO: "(instruction->operand1 & UINT32_MAX)", | ||
HoleValue.TARGET: "instruction->target", | ||
HoleValue.JUMP_TARGET: "state->instruction_starts[instruction->jump_target]", | ||
HoleValue.ERROR_TARGET: "state->instruction_starts[instruction->error_target]", | ||
# These should all have raised an error if they were actually used: | ||
# HoleValue.WRITABLE: "", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I fully understand this. Can you expland please? |
||
HoleValue.ZERO: "", | ||
} | ||
|
||
|
@@ -246,6 +251,12 @@ def process_relocations(self, known_symbols: dict[str, int]) -> None: | |
self.data.pad(8) | ||
for stencil in [self.code, self.data]: | ||
for hole in stencil.holes: | ||
if hole.symbol in self.symbols: | ||
value, _ = self.symbols[hole.symbol] | ||
if value is HoleValue.WRITABLE: | ||
raise ValueError( | ||
f"Writable data ({hole.symbol}) is not supported!" | ||
) | ||
if hole.value is HoleValue.GOT: | ||
assert hole.symbol is not None | ||
hole.value = HoleValue.DATA | ||
|
Uh oh!
There was an error while loading. Please reload this page.