Skip to content

Commit bf1b7c8

Browse files
authored
[mypyc] Support _benchmark test suffix for optimized run tests (#21047)
If a mypyc run test has `_benchmark` in the test name as a suffix (not necessarily the final suffix), the test case will be compiled with optimizations enabled, and this also applies to `librt` if it needs to be built. The idea is that this can be used to easily run `librt` benchmarks by using temporary throwaway tests. Tested by manually running some test cases with and without a `_benchmark` suffix.
1 parent 8fae935 commit bf1b7c8

File tree

3 files changed

+32
-16
lines changed

3 files changed

+32
-16
lines changed

mypyc/test/librt_cache.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,15 @@
3333
from mypyc.common import RUNTIME_C_FILES
3434

3535

36-
def _librt_build_hash(experimental: bool) -> str:
36+
def _librt_build_hash(experimental: bool, opt_level: str) -> str:
3737
"""Compute hash for librt build, including sources and build environment."""
3838
# Import lazily to ensure mypyc.build has ensured that distutils is correctly set up
3939
from distutils import ccompiler
4040

4141
h = hashlib.sha256()
4242
# Include experimental flag
4343
h.update(b"exp" if experimental else b"noexp")
44+
h.update(f"opt={opt_level}".encode())
4445
# Include full Python version string (includes git hash for dev builds)
4546
h.update(sys.version.encode())
4647
# Include debug build status (gettotalrefcount only exists in debug builds)
@@ -72,16 +73,16 @@ def _librt_build_hash(experimental: bool) -> str:
7273
return h.hexdigest()[:16]
7374

7475

75-
def _generate_setup_py(build_dir: str, experimental: bool) -> str:
76+
def _generate_setup_py(build_dir: str, experimental: bool, opt_level: str) -> str:
7677
"""Generate setup.py content for building librt directly.
7778
7879
We inline LIBRT_MODULES/RUNTIME_C_FILES/include_dir/cflags values to avoid
7980
importing mypyc.build, which recursively imports lots of things.
8081
"""
8182
lib_rt_dir = include_dir()
8283

83-
# Get compiler flags using the shared helper (with -O0 for faster builds)
84-
cflags = get_cflags(opt_level="0", experimental_features=experimental)
84+
# Get compiler flags using the shared helper
85+
cflags = get_cflags(opt_level=opt_level, experimental_features=experimental)
8586

8687
# Serialize values to inline in generated setup.py
8788
librt_modules_repr = repr(
@@ -135,7 +136,7 @@ def write_file(path, contents):
135136
"""
136137

137138

138-
def get_librt_path(experimental: bool = True) -> str:
139+
def get_librt_path(experimental: bool = True, opt_level: str = "0") -> str:
139140
"""Get path to librt built from the repository, building and caching if necessary.
140141
141142
Uses build/librt-cache/ under the repo root (gitignored). The cache is
@@ -146,14 +147,15 @@ def get_librt_path(experimental: bool = True) -> str:
146147
147148
Args:
148149
experimental: Whether to enable experimental features.
150+
opt_level: Optimization level ("0".."3") used when building librt.
149151
150152
Returns:
151153
Path to directory containing built librt modules.
152154
"""
153155
# Use build/librt-cache/ under the repo root (gitignored)
154156
repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
155157
cache_root = os.path.join(repo_root, "build", "librt-cache")
156-
build_hash = _librt_build_hash(experimental)
158+
build_hash = _librt_build_hash(experimental, opt_level)
157159
build_dir = os.path.join(cache_root, f"librt-{build_hash}")
158160
lock_file = os.path.join(cache_root, f"librt-{build_hash}.lock")
159161
marker = os.path.join(build_dir, ".complete")
@@ -186,7 +188,7 @@ def get_librt_path(experimental: bool = True) -> str:
186188
# Write setup.py
187189
setup_py = os.path.join(build_dir, "setup.py")
188190
with open(setup_py, "w") as f:
189-
f.write(_generate_setup_py(build_dir, experimental))
191+
f.write(_generate_setup_py(build_dir, experimental, opt_level))
190192

191193
# Build (parallel builds don't work well because multiple extensions
192194
# share the same runtime C files, causing race conditions)
@@ -207,7 +209,7 @@ def get_librt_path(experimental: bool = True) -> str:
207209

208210

209211
def run_with_librt(
210-
file_path: str, experimental: bool = True, check: bool = True
212+
file_path: str, experimental: bool = True, check: bool = True, opt_level: str = "0"
211213
) -> subprocess.CompletedProcess[str]:
212214
"""Run a Python file in a subprocess with built librt available.
213215
@@ -218,11 +220,12 @@ def run_with_librt(
218220
file_path: Path to Python file to execute.
219221
experimental: Whether to use experimental features.
220222
check: If True, raise CalledProcessError on non-zero exit.
223+
opt_level: Optimization level ("0".."3") used when building librt.
221224
222225
Returns:
223226
CompletedProcess with stdout, stderr, and returncode.
224227
"""
225-
librt_path = get_librt_path(experimental)
228+
librt_path = get_librt_path(experimental, opt_level=opt_level)
226229
# Prepend librt path to PYTHONPATH
227230
env = os.environ.copy()
228231
existing = env.get("PYTHONPATH", "")

mypyc/test/test_run.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
MypycDataSuite,
3737
assert_test_output,
3838
fudge_dir_mtimes,
39+
has_test_name_tag,
3940
show_c,
4041
use_custom_builtins,
4142
)
@@ -210,7 +211,10 @@ def run_case_inner(self, testcase: DataDrivenTestCase) -> None:
210211
self.run_case_step(testcase, step)
211212

212213
def run_case_step(self, testcase: DataDrivenTestCase, incremental_step: int) -> None:
213-
bench = testcase.config.getoption("--bench", False) and "Benchmark" in testcase.name
214+
benchmark_build = has_test_name_tag(testcase.name, "benchmark")
215+
bench = testcase.config.getoption("--bench", False) and (
216+
benchmark_build or "Benchmark" in testcase.name
217+
)
214218

215219
options = Options()
216220
options.use_builtins_fixtures = True
@@ -262,10 +266,10 @@ def run_case_step(self, testcase: DataDrivenTestCase, incremental_step: int) ->
262266

263267
# Use _librt_internal to test mypy-specific parts of librt (they have
264268
# some special-casing in mypyc), for everything else use _librt suffix.
265-
librt_internal = testcase.name.endswith("_librt_internal")
266-
librt = testcase.name.endswith("_librt") or "_librt_" in testcase.name
269+
librt_internal = has_test_name_tag(testcase.name, "librt_internal")
270+
librt = has_test_name_tag(testcase.name, "librt")
267271
# Enable experimental features (local librt build also includes experimental features)
268-
experimental_features = testcase.name.endswith("_experimental")
272+
experimental_features = has_test_name_tag(testcase.name, "experimental")
269273
try:
270274
compiler_options = CompilerOptions(
271275
multi_file=self.multi_file,
@@ -302,7 +306,7 @@ def run_case_step(self, testcase: DataDrivenTestCase, incremental_step: int) ->
302306
if incremental_step == 1:
303307
check_serialization_roundtrip(ir)
304308

305-
opt_level = int(os.environ.get("MYPYC_OPT_LEVEL", 0))
309+
opt_level = 3 if benchmark_build else int(os.environ.get("MYPYC_OPT_LEVEL", 0))
306310

307311
setup_file = os.path.abspath(os.path.join(WORKDIR, "setup.py"))
308312
# We pass the C file information to the build script via setup.py unfortunately
@@ -322,7 +326,7 @@ def run_case_step(self, testcase: DataDrivenTestCase, incremental_step: int) ->
322326

323327
if librt:
324328
# Use cached pre-built librt instead of rebuilding for each test
325-
cached_librt = get_librt_path(experimental_features)
329+
cached_librt = get_librt_path(experimental_features, opt_level=str(opt_level))
326330
shutil.copytree(os.path.join(cached_librt, "librt"), "librt")
327331

328332
if not run_setup(setup_file, ["build_ext", "--inplace"]):

mypyc/test/testutil.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,15 @@ def infer_ir_build_options_from_test_name(name: str) -> CompilerOptions | None:
286286
options.python_version = options.capi_version
287287
elif "_py" in name or "_Python" in name:
288288
assert False, f"Invalid _py* suffix (should be _pythonX_Y): {name}"
289-
if re.search("_experimental(_|$)", name):
289+
if has_test_name_tag(name, "experimental"):
290290
options.experimental_features = True
291291
return options
292+
293+
294+
def has_test_name_tag(name: str, tag: str) -> bool:
295+
"""Check if a test case name contains a tag token like ``_experimental``.
296+
297+
A tag matches if it appears as a full underscore-delimited token:
298+
``foo_tag_bar`` or ``foo_tag``.
299+
"""
300+
return re.search(rf"(?:^|_){re.escape(tag)}(?:_|$)", name) is not None

0 commit comments

Comments
 (0)